mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
Rename ExecutionEnvironment to Sandbox across the codebase
Adopt cleaner terminology: Sandbox (resource providing disk + execution), SandboxProvider (Local/Docker/Daytona), SandboxEvent (lifecycle events), and Snapshot (pre-built environment images). Flatten DaytonaSandboxConfig into DaytonaConfig, rename CLI flag to --sandbox, and update TOML config sections from [execution] to [sandbox]. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ffa9d1d505
commit
d0c8bd3eed
48 changed files with 806 additions and 816 deletions
|
|
@ -9,7 +9,7 @@ The crate is organized around a central `Session` that drives an agentic loop:
|
|||
1. **User input** is appended to a conversation `History`
|
||||
2. The session builds a `Request` with system prompt, history, and tools
|
||||
3. An LLM generates a response (text and/or tool calls) via `unified-llm`
|
||||
4. Tool calls are executed through a `ToolRegistry` against an `ExecutionEnvironment`
|
||||
4. Tool calls are executed through a `ToolRegistry` against an `Sandbox`
|
||||
5. Results are recorded and the loop continues until the LLM responds with text only (natural completion), a turn limit is reached, or the session is aborted
|
||||
|
||||
```
|
||||
|
|
@ -41,7 +41,7 @@ User Input
|
|||
|
||||
- **`Session`** -- Manages the full agentic loop: LLM calls, tool execution, steering, follow-ups, abort handling, and event emission.
|
||||
- **`ProviderProfile`** (trait) -- Defines how to build system prompts, which tools to register, and what capabilities a provider supports. Ships with `AnthropicProfile`, `OpenAiProfile`, and `GeminiProfile`.
|
||||
- **`ExecutionEnvironment`** (trait) -- Abstracts filesystem, shell, grep, and glob operations. `LocalExecutionEnvironment` provides a real implementation; the trait enables sandboxing and testing.
|
||||
- **`Sandbox`** (trait) -- Abstracts filesystem, shell, grep, and glob operations. `LocalSandbox` provides a real implementation; the trait enables sandboxing and testing.
|
||||
- **`ToolRegistry`** -- Maps tool names to definitions and async executor functions. Tools are registered per-profile.
|
||||
- **`History`** -- Ordered list of `Turn` variants (`User`, `Assistant`, `ToolResults`, `System`, `Steering`) that converts to LLM messages.
|
||||
- **`EventEmitter`** -- Broadcasts `SessionEvent`s (tool calls, text, errors, warnings) over a `tokio::sync::broadcast` channel for UI or logging.
|
||||
|
|
@ -52,7 +52,7 @@ User Input
|
|||
|
||||
### `Session`
|
||||
|
||||
The main entry point. Created with an LLM client, a provider profile, an execution environment, and a config.
|
||||
The main entry point. Created with an LLM client, a provider profile, a sandbox, and a config.
|
||||
|
||||
### `ProviderProfile`
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ pub trait ProviderProfile: Send + Sync {
|
|||
fn tool_registry(&self) -> &ToolRegistry;
|
||||
fn build_system_prompt(
|
||||
&self,
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env: &dyn Sandbox,
|
||||
env_context: &EnvContext,
|
||||
project_docs: &[String],
|
||||
user_instructions: Option<&str>,
|
||||
|
|
@ -79,10 +79,10 @@ Built-in profiles:
|
|||
- **`OpenAiProfile`** -- 128K context, reasoning effort support, tools: `read_file`, `write_file`, `shell`, `grep`, `glob`, `apply_patch` (v4a format)
|
||||
- **`GeminiProfile`** -- 1M context, safety settings, tools: all Anthropic tools plus `read_many_files`, `list_dir`, `web_search`, `web_fetch`
|
||||
|
||||
### `ExecutionEnvironment`
|
||||
### `Sandbox`
|
||||
|
||||
```rust
|
||||
pub trait ExecutionEnvironment: Send + Sync {
|
||||
pub trait Sandbox: Send + Sync {
|
||||
async fn read_file(&self, path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String, String>;
|
||||
async fn write_file(&self, path: &str, content: &str) -> Result<(), String>;
|
||||
async fn exec_command(&self, command: &str, timeout_ms: u64, ...) -> Result<ExecResult, String>;
|
||||
|
|
@ -92,7 +92,7 @@ pub trait ExecutionEnvironment: Send + Sync {
|
|||
}
|
||||
```
|
||||
|
||||
`LocalExecutionEnvironment` is the real implementation with env-var filtering (strips secrets), process group management, and ripgrep/grep fallback.
|
||||
`LocalSandbox` is the real implementation with env-var filtering (strips secrets), process group management, and ripgrep/grep fallback.
|
||||
|
||||
### `SessionConfig`
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ pub struct SessionConfig {
|
|||
|
||||
```rust
|
||||
use agent::{
|
||||
AnthropicProfile, LocalExecutionEnvironment, Session, SessionConfig,
|
||||
AnthropicProfile, LocalSandbox, Session, SessionConfig,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -127,8 +127,8 @@ let client: Client = /* configure unified-llm client */;
|
|||
// 2. Choose a provider profile
|
||||
let profile = Arc::new(AnthropicProfile::new("claude-sonnet-4-20250514"));
|
||||
|
||||
// 3. Create an execution environment
|
||||
let env = Arc::new(LocalExecutionEnvironment::new(
|
||||
// 3. Create a sandbox
|
||||
let env = Arc::new(LocalSandbox::new(
|
||||
PathBuf::from("/path/to/project"),
|
||||
));
|
||||
|
||||
|
|
@ -232,6 +232,6 @@ profile.register_subagent_tools(manager, factory, 0);
|
|||
- **Context window monitoring** -- Emits `ContextWindowWarning` events when estimated usage exceeds 80%
|
||||
- **Tool argument validation** -- Validates arguments against JSON Schema before execution
|
||||
- **Tool output truncation** -- Per-tool character and line limits with head/tail or tail-only truncation modes
|
||||
- **Environment variable filtering** -- `LocalExecutionEnvironment` strips secrets (`*_API_KEY`, `*_SECRET`, `*_TOKEN`, `*_PASSWORD`, `*_CREDENTIAL`) from subprocess environments
|
||||
- **Environment variable filtering** -- `LocalSandbox` strips secrets (`*_API_KEY`, `*_SECRET`, `*_TOKEN`, `*_PASSWORD`, `*_CREDENTIAL`) from subprocess environments
|
||||
- **Command timeouts** -- Configurable per-command with process group cleanup (SIGTERM then SIGKILL)
|
||||
- **Project doc discovery** -- Automatically discovers `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, or `.codex/instructions.md` based on provider, with a 32KB budget
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
subagent::{SessionFactory, SubAgentManager},
|
||||
AgentEvent, AnthropicProfile, GeminiProfile, LocalExecutionEnvironment, OpenAiProfile,
|
||||
AgentEvent, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile,
|
||||
ProviderProfile, Session, SessionConfig, ToolApprovalFn, Turn,
|
||||
};
|
||||
use arc_llm::client::Client;
|
||||
|
|
@ -385,7 +385,7 @@ pub async fn run_with_args(args: AgentArgs) -> anyhow::Result<()> {
|
|||
// Build execution environment
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let cwd_str = cwd.to_string_lossy().to_string();
|
||||
let env: Arc<dyn crate::ExecutionEnvironment> = Arc::new(LocalExecutionEnvironment::new(cwd));
|
||||
let env: Arc<dyn crate::Sandbox> = Arc::new(LocalSandbox::new(cwd));
|
||||
|
||||
// Build tool approval callback
|
||||
let is_interactive = std::io::stdin().is_terminal() && !args.auto_approve;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::execution_env::{
|
||||
format_lines_numbered, DirEntry, ExecEnvEventCallback, ExecResult, ExecutionEnvEvent,
|
||||
ExecutionEnvironment, GrepOptions,
|
||||
use crate::sandbox::{
|
||||
format_lines_numbered, DirEntry, SandboxEventCallback, ExecResult, SandboxEvent,
|
||||
Sandbox, GrepOptions,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use bollard::container::{
|
||||
|
|
@ -16,7 +16,7 @@ use std::time::Instant;
|
|||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// Configuration for a Docker-based execution environment.
|
||||
pub struct DockerConfig {
|
||||
pub struct DockerSandboxConfig {
|
||||
/// Docker image to use. Default: `"arc-agent:latest"`.
|
||||
pub image: String,
|
||||
/// Host directory to bind-mount into the container.
|
||||
|
|
@ -37,7 +37,7 @@ pub struct DockerConfig {
|
|||
pub env_vars: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for DockerConfig {
|
||||
impl Default for DockerSandboxConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
image: "arc-agent:latest".to_string(),
|
||||
|
|
@ -57,22 +57,22 @@ impl Default for DockerConfig {
|
|||
///
|
||||
/// The host working directory is bind-mounted at `container_mount_point`. All file
|
||||
/// operations, commands, grep, and glob execute inside the container via `docker exec`.
|
||||
pub struct DockerExecutionEnvironment {
|
||||
pub struct DockerSandbox {
|
||||
docker: Docker,
|
||||
config: DockerConfig,
|
||||
config: DockerSandboxConfig,
|
||||
container_id: tokio::sync::OnceCell<String>,
|
||||
cached_platform: std::sync::OnceLock<String>,
|
||||
cached_os_version: std::sync::OnceLock<String>,
|
||||
rg_available: tokio::sync::OnceCell<bool>,
|
||||
event_callback: Option<ExecEnvEventCallback>,
|
||||
event_callback: Option<SandboxEventCallback>,
|
||||
}
|
||||
|
||||
impl DockerExecutionEnvironment {
|
||||
/// Creates a new `DockerExecutionEnvironment`.
|
||||
impl DockerSandbox {
|
||||
/// Creates a new `DockerSandbox`.
|
||||
///
|
||||
/// Validates Docker daemon connectivity but does NOT create a container.
|
||||
/// Call `initialize()` to create and start the container.
|
||||
pub fn new(config: DockerConfig) -> Result<Self, String> {
|
||||
pub fn new(config: DockerSandboxConfig) -> Result<Self, String> {
|
||||
let docker = Docker::connect_with_local_defaults()
|
||||
.map_err(|e| format!("Failed to connect to Docker daemon: {e}"))?;
|
||||
Ok(Self {
|
||||
|
|
@ -86,11 +86,11 @@ impl DockerExecutionEnvironment {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn set_event_callback(&mut self, cb: ExecEnvEventCallback) {
|
||||
pub fn set_event_callback(&mut self, cb: SandboxEventCallback) {
|
||||
self.event_callback = Some(cb);
|
||||
}
|
||||
|
||||
fn emit(&self, event: ExecutionEnvEvent) {
|
||||
fn emit(&self, event: SandboxEvent) {
|
||||
event.trace();
|
||||
if let Some(ref cb) = self.event_callback {
|
||||
cb(event);
|
||||
|
|
@ -269,29 +269,29 @@ impl DockerExecutionEnvironment {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionEnvironment for DockerExecutionEnvironment {
|
||||
impl Sandbox for DockerSandbox {
|
||||
async fn initialize(&self) -> Result<(), String> {
|
||||
self.emit(ExecutionEnvEvent::Initializing {
|
||||
env_type: "docker".into(),
|
||||
self.emit(SandboxEvent::Initializing {
|
||||
provider: "docker".into(),
|
||||
});
|
||||
let init_start = Instant::now();
|
||||
|
||||
self.emit(ExecutionEnvEvent::ImagePulling {
|
||||
image: self.config.image.clone(),
|
||||
self.emit(SandboxEvent::SnapshotPulling {
|
||||
name: self.config.image.clone(),
|
||||
});
|
||||
let pull_start = Instant::now();
|
||||
if let Err(e) = self.ensure_image().await {
|
||||
let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::InitializeFailed {
|
||||
env_type: "docker".into(),
|
||||
self.emit(SandboxEvent::InitializeFailed {
|
||||
provider: "docker".into(),
|
||||
error: e.clone(),
|
||||
duration_ms,
|
||||
});
|
||||
return Err(e);
|
||||
}
|
||||
let pull_duration = u64::try_from(pull_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::ImagePulled {
|
||||
image: self.config.image.clone(),
|
||||
self.emit(SandboxEvent::SnapshotPulled {
|
||||
name: self.config.image.clone(),
|
||||
duration_ms: pull_duration,
|
||||
});
|
||||
|
||||
|
|
@ -361,8 +361,8 @@ impl ExecutionEnvironment for DockerExecutionEnvironment {
|
|||
.set(format!("linux {}", uname_output.trim()));
|
||||
|
||||
let init_duration = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::Ready {
|
||||
env_type: "docker".into(),
|
||||
self.emit(SandboxEvent::Ready {
|
||||
provider: "docker".into(),
|
||||
duration_ms: init_duration,
|
||||
});
|
||||
|
||||
|
|
@ -370,8 +370,8 @@ impl ExecutionEnvironment for DockerExecutionEnvironment {
|
|||
}
|
||||
|
||||
async fn cleanup(&self) -> Result<(), String> {
|
||||
self.emit(ExecutionEnvEvent::CleanupStarted {
|
||||
env_type: "docker".into(),
|
||||
self.emit(SandboxEvent::CleanupStarted {
|
||||
provider: "docker".into(),
|
||||
});
|
||||
let start = Instant::now();
|
||||
|
||||
|
|
@ -379,8 +379,8 @@ impl ExecutionEnvironment for DockerExecutionEnvironment {
|
|||
Some(id) => id.clone(),
|
||||
None => {
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::CleanupCompleted {
|
||||
env_type: "docker".into(),
|
||||
self.emit(SandboxEvent::CleanupCompleted {
|
||||
provider: "docker".into(),
|
||||
duration_ms,
|
||||
});
|
||||
return Ok(());
|
||||
|
|
@ -405,8 +405,8 @@ impl ExecutionEnvironment for DockerExecutionEnvironment {
|
|||
.await;
|
||||
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::CleanupCompleted {
|
||||
env_type: "docker".into(),
|
||||
self.emit(SandboxEvent::CleanupCompleted {
|
||||
provider: "docker".into(),
|
||||
duration_ms,
|
||||
});
|
||||
|
||||
|
|
@ -720,8 +720,8 @@ mod tests {
|
|||
Docker::connect_with_local_defaults().expect("Docker not available — skipping")
|
||||
}
|
||||
|
||||
fn test_config(host_dir: &str) -> DockerConfig {
|
||||
DockerConfig {
|
||||
fn test_config(host_dir: &str) -> DockerSandboxConfig {
|
||||
DockerSandboxConfig {
|
||||
host_working_directory: host_dir.to_string(),
|
||||
auto_pull: false,
|
||||
..Default::default()
|
||||
|
|
@ -737,8 +737,8 @@ mod tests {
|
|||
std::fs::create_dir_all(&host_dir).unwrap();
|
||||
|
||||
let config = test_config(host_dir.to_str().unwrap());
|
||||
let env: Arc<dyn ExecutionEnvironment> =
|
||||
Arc::new(DockerExecutionEnvironment::new(config).unwrap());
|
||||
let env: Arc<dyn Sandbox> =
|
||||
Arc::new(DockerSandbox::new(config).unwrap());
|
||||
|
||||
// Initialize
|
||||
env.initialize().await.unwrap();
|
||||
|
|
@ -803,7 +803,7 @@ mod tests {
|
|||
std::fs::create_dir_all(&host_dir).unwrap();
|
||||
|
||||
let config = test_config(host_dir.to_str().unwrap());
|
||||
let env = DockerExecutionEnvironment::new(config).unwrap();
|
||||
let env = DockerSandbox::new(config).unwrap();
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
let result = env
|
||||
|
|
@ -826,7 +826,7 @@ mod tests {
|
|||
std::fs::create_dir_all(&host_dir).unwrap();
|
||||
|
||||
let config = test_config(host_dir.to_str().unwrap());
|
||||
let env = DockerExecutionEnvironment::new(config).unwrap();
|
||||
let env = DockerSandbox::new(config).unwrap();
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
let content = "hello \"world\"\nit's a `test`\nprice: $100\nbackslash: \\\nnewline above";
|
||||
|
|
@ -852,7 +852,7 @@ mod tests {
|
|||
std::fs::create_dir_all(&host_dir).unwrap();
|
||||
|
||||
let config = test_config(host_dir.to_str().unwrap());
|
||||
let env = DockerExecutionEnvironment::new(config).unwrap();
|
||||
let env = DockerSandbox::new(config).unwrap();
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
// Relative path resolves to container_mount_point
|
||||
|
|
@ -879,7 +879,7 @@ mod tests {
|
|||
std::fs::create_dir_all(&host_dir).unwrap();
|
||||
|
||||
let config = test_config(host_dir.to_str().unwrap());
|
||||
let env = DockerExecutionEnvironment::new(config).unwrap();
|
||||
let env = DockerSandbox::new(config).unwrap();
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
// First cleanup
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
#[cfg(feature = "docker")]
|
||||
pub mod docker_env;
|
||||
pub mod docker_sandbox;
|
||||
|
||||
pub mod cli;
|
||||
pub mod compaction;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod execution_env;
|
||||
pub mod file_tracker;
|
||||
pub mod history;
|
||||
pub mod local_env;
|
||||
pub mod local_sandbox;
|
||||
pub mod loop_detection;
|
||||
pub mod mcp_integration;
|
||||
pub mod profiles;
|
||||
pub mod project_docs;
|
||||
pub mod provider_profile;
|
||||
pub mod read_before_write_env;
|
||||
pub mod read_before_write_sandbox;
|
||||
pub mod sandbox;
|
||||
pub mod session;
|
||||
pub mod skills;
|
||||
pub mod subagent;
|
||||
|
|
@ -29,20 +29,20 @@ pub mod v4a_patch;
|
|||
pub use arc_mcp::config::McpServerConfig;
|
||||
pub use config::{SessionConfig, ToolApprovalFn};
|
||||
#[cfg(feature = "docker")]
|
||||
pub use docker_env::{DockerConfig, DockerExecutionEnvironment};
|
||||
pub use docker_sandbox::{DockerSandboxConfig, DockerSandbox};
|
||||
pub use error::AgentError;
|
||||
pub use event::EventEmitter;
|
||||
pub use execution_env::{
|
||||
format_lines_numbered, DirEntry, ExecEnvEventCallback, ExecResult, ExecutionEnvEvent,
|
||||
ExecutionEnvironment, GrepOptions,
|
||||
pub use sandbox::{
|
||||
format_lines_numbered, DirEntry, SandboxEventCallback, ExecResult, SandboxEvent,
|
||||
Sandbox, GrepOptions,
|
||||
};
|
||||
pub use history::History;
|
||||
pub use local_env::LocalExecutionEnvironment;
|
||||
pub use local_sandbox::LocalSandbox;
|
||||
pub use loop_detection::detect_loop;
|
||||
pub use profiles::{AnthropicProfile, EnvContext, GeminiProfile, OpenAiProfile};
|
||||
pub use project_docs::discover_project_docs;
|
||||
pub use provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
pub use read_before_write_env::ReadBeforeWriteEnvironment;
|
||||
pub use read_before_write_sandbox::ReadBeforeWriteSandbox;
|
||||
pub use session::Session;
|
||||
pub use skills::Skill;
|
||||
pub use subagent::{SubAgent, SubAgentEventCallback, SubAgentManager, SubAgentResult};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::execution_env::{
|
||||
format_lines_numbered, DirEntry, ExecEnvEventCallback, ExecResult, ExecutionEnvEvent,
|
||||
ExecutionEnvironment, GrepOptions,
|
||||
use crate::sandbox::{
|
||||
format_lines_numbered, DirEntry, SandboxEventCallback, ExecResult, SandboxEvent,
|
||||
Sandbox, GrepOptions,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -9,13 +9,13 @@ use tokio::io::AsyncReadExt;
|
|||
use tokio::process::Command;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub struct LocalExecutionEnvironment {
|
||||
pub struct LocalSandbox {
|
||||
working_directory: PathBuf,
|
||||
event_callback: Option<ExecEnvEventCallback>,
|
||||
event_callback: Option<SandboxEventCallback>,
|
||||
rg_available: std::sync::OnceLock<bool>,
|
||||
}
|
||||
|
||||
impl LocalExecutionEnvironment {
|
||||
impl LocalSandbox {
|
||||
#[must_use]
|
||||
pub fn new(working_directory: PathBuf) -> Self {
|
||||
Self {
|
||||
|
|
@ -25,11 +25,11 @@ impl LocalExecutionEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_event_callback(&mut self, cb: ExecEnvEventCallback) {
|
||||
pub fn set_event_callback(&mut self, cb: SandboxEventCallback) {
|
||||
self.event_callback = Some(cb);
|
||||
}
|
||||
|
||||
fn emit(&self, event: ExecutionEnvEvent) {
|
||||
fn emit(&self, event: SandboxEvent) {
|
||||
event.trace();
|
||||
if let Some(ref cb) = self.event_callback {
|
||||
cb(event);
|
||||
|
|
@ -72,7 +72,7 @@ impl LocalExecutionEnvironment {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionEnvironment for LocalExecutionEnvironment {
|
||||
impl Sandbox for LocalSandbox {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &str,
|
||||
|
|
@ -346,8 +346,8 @@ impl ExecutionEnvironment for LocalExecutionEnvironment {
|
|||
}
|
||||
|
||||
async fn initialize(&self) -> Result<(), String> {
|
||||
self.emit(ExecutionEnvEvent::Initializing {
|
||||
env_type: "local".into(),
|
||||
self.emit(SandboxEvent::Initializing {
|
||||
provider: "local".into(),
|
||||
});
|
||||
let start = Instant::now();
|
||||
let result = tokio::fs::create_dir_all(&self.working_directory)
|
||||
|
|
@ -355,12 +355,12 @@ impl ExecutionEnvironment for LocalExecutionEnvironment {
|
|||
.map_err(|e| format!("Failed to create working directory: {e}"));
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
match &result {
|
||||
Ok(()) => self.emit(ExecutionEnvEvent::Ready {
|
||||
env_type: "local".into(),
|
||||
Ok(()) => self.emit(SandboxEvent::Ready {
|
||||
provider: "local".into(),
|
||||
duration_ms,
|
||||
}),
|
||||
Err(e) => self.emit(ExecutionEnvEvent::InitializeFailed {
|
||||
env_type: "local".into(),
|
||||
Err(e) => self.emit(SandboxEvent::InitializeFailed {
|
||||
provider: "local".into(),
|
||||
error: e.clone(),
|
||||
duration_ms,
|
||||
}),
|
||||
|
|
@ -369,13 +369,13 @@ impl ExecutionEnvironment for LocalExecutionEnvironment {
|
|||
}
|
||||
|
||||
async fn cleanup(&self) -> Result<(), String> {
|
||||
self.emit(ExecutionEnvEvent::CleanupStarted {
|
||||
env_type: "local".into(),
|
||||
self.emit(SandboxEvent::CleanupStarted {
|
||||
provider: "local".into(),
|
||||
});
|
||||
let start = Instant::now();
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::CleanupCompleted {
|
||||
env_type: "local".into(),
|
||||
self.emit(SandboxEvent::CleanupCompleted {
|
||||
provider: "local".into(),
|
||||
duration_ms,
|
||||
});
|
||||
Ok(())
|
||||
|
|
@ -457,7 +457,7 @@ mod tests {
|
|||
let dir = temp_dir();
|
||||
std::fs::write(dir.join("test.txt"), "hello\nworld\nfoo").unwrap();
|
||||
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let result = env.read_file("test.txt", None, None).await.unwrap();
|
||||
|
||||
assert_eq!(result, "1 | hello\n2 | world\n3 | foo\n");
|
||||
|
|
@ -470,7 +470,7 @@ mod tests {
|
|||
let content: String = (1..=12).map(|i| format!("line {i}\n")).collect();
|
||||
std::fs::write(dir.join("padded.txt"), content.trim_end()).unwrap();
|
||||
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let result = env.read_file("padded.txt", None, None).await.unwrap();
|
||||
|
||||
assert!(result.starts_with(" 1 | line 1\n"));
|
||||
|
|
@ -481,7 +481,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn read_file_not_found() {
|
||||
let dir = temp_dir();
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let result = env.read_file("nonexistent.txt", None, None).await;
|
||||
assert!(result.is_err());
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
|
|
@ -490,7 +490,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn write_file_creates_parent_dirs() {
|
||||
let dir = temp_dir();
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
env.write_file("sub/dir/test.txt", "content").await.unwrap();
|
||||
|
||||
let written = std::fs::read_to_string(dir.join("sub/dir/test.txt")).unwrap();
|
||||
|
|
@ -503,7 +503,7 @@ mod tests {
|
|||
let dir = temp_dir();
|
||||
std::fs::write(dir.join("exists.txt"), "data").unwrap();
|
||||
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
assert!(env.file_exists("exists.txt").await.unwrap());
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
|
@ -511,7 +511,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn file_exists_false() {
|
||||
let dir = temp_dir();
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
assert!(!env.file_exists("nope.txt").await.unwrap());
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
|
@ -523,7 +523,7 @@ mod tests {
|
|||
std::fs::write(dir.join("a.txt"), "a").unwrap();
|
||||
std::fs::create_dir(dir.join("c_dir")).unwrap();
|
||||
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let entries = env.list_directory(".", None).await.unwrap();
|
||||
|
||||
assert_eq!(entries.len(), 3);
|
||||
|
|
@ -540,7 +540,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn exec_command_echo() {
|
||||
let dir = temp_dir();
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let result = env
|
||||
.exec_command("echo hello", 5000, None, None, None)
|
||||
.await
|
||||
|
|
@ -556,7 +556,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn exec_command_exit_code() {
|
||||
let dir = temp_dir();
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let result = env
|
||||
.exec_command("exit 42", 5000, None, None, None)
|
||||
.await
|
||||
|
|
@ -570,7 +570,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn exec_command_timeout() {
|
||||
let dir = temp_dir();
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let result = env
|
||||
.exec_command("sleep 10", 200, None, None, None)
|
||||
.await
|
||||
|
|
@ -584,7 +584,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn exec_command_stderr() {
|
||||
let dir = temp_dir();
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let result = env
|
||||
.exec_command("echo err >&2", 5000, None, None, None)
|
||||
.await
|
||||
|
|
@ -596,43 +596,43 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn env_var_filtering() {
|
||||
assert!(LocalExecutionEnvironment::should_filter_env_var(
|
||||
assert!(LocalSandbox::should_filter_env_var(
|
||||
"OPENAI_API_KEY"
|
||||
));
|
||||
assert!(LocalExecutionEnvironment::should_filter_env_var(
|
||||
assert!(LocalSandbox::should_filter_env_var(
|
||||
"ANTHROPIC_API_KEY"
|
||||
));
|
||||
assert!(LocalExecutionEnvironment::should_filter_env_var(
|
||||
assert!(LocalSandbox::should_filter_env_var(
|
||||
"DB_PASSWORD"
|
||||
));
|
||||
assert!(LocalExecutionEnvironment::should_filter_env_var(
|
||||
assert!(LocalSandbox::should_filter_env_var(
|
||||
"AWS_SECRET"
|
||||
));
|
||||
assert!(LocalExecutionEnvironment::should_filter_env_var(
|
||||
assert!(LocalSandbox::should_filter_env_var(
|
||||
"AUTH_TOKEN"
|
||||
));
|
||||
assert!(LocalExecutionEnvironment::should_filter_env_var(
|
||||
assert!(LocalSandbox::should_filter_env_var(
|
||||
"MY_CREDENTIAL"
|
||||
));
|
||||
// Case insensitive
|
||||
assert!(LocalExecutionEnvironment::should_filter_env_var(
|
||||
assert!(LocalSandbox::should_filter_env_var(
|
||||
"my_api_key"
|
||||
));
|
||||
assert!(LocalExecutionEnvironment::should_filter_env_var(
|
||||
assert!(LocalSandbox::should_filter_env_var(
|
||||
"Some_Secret"
|
||||
));
|
||||
// Should not filter
|
||||
assert!(!LocalExecutionEnvironment::should_filter_env_var("PATH"));
|
||||
assert!(!LocalExecutionEnvironment::should_filter_env_var("HOME"));
|
||||
assert!(!LocalExecutionEnvironment::should_filter_env_var("EDITOR"));
|
||||
assert!(!LocalExecutionEnvironment::should_filter_env_var(
|
||||
assert!(!LocalSandbox::should_filter_env_var("PATH"));
|
||||
assert!(!LocalSandbox::should_filter_env_var("HOME"));
|
||||
assert!(!LocalSandbox::should_filter_env_var("EDITOR"));
|
||||
assert!(!LocalSandbox::should_filter_env_var(
|
||||
"SECRET_PATH"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_is_known() {
|
||||
let env = LocalExecutionEnvironment::new(PathBuf::from("/tmp"));
|
||||
let env = LocalSandbox::new(PathBuf::from("/tmp"));
|
||||
let platform = env.platform();
|
||||
assert!(
|
||||
platform == "darwin" || platform == "linux" || platform == "windows",
|
||||
|
|
@ -642,7 +642,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn os_version_contains_platform() {
|
||||
let env = LocalExecutionEnvironment::new(PathBuf::from("/tmp"));
|
||||
let env = LocalSandbox::new(PathBuf::from("/tmp"));
|
||||
let version = env.os_version();
|
||||
assert!(
|
||||
version.contains(env.platform()),
|
||||
|
|
@ -652,14 +652,14 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn working_directory_accessor() {
|
||||
let env = LocalExecutionEnvironment::new(PathBuf::from("/tmp/test_dir"));
|
||||
let env = LocalSandbox::new(PathBuf::from("/tmp/test_dir"));
|
||||
assert_eq!(env.working_directory(), "/tmp/test_dir");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_creates_directory() {
|
||||
let dir = std::env::temp_dir().join(format!("init_test_{}", uuid::Uuid::new_v4()));
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
env.initialize().await.unwrap();
|
||||
assert!(dir.exists());
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
|
|
@ -667,14 +667,14 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn initialize_emits_events() {
|
||||
use crate::execution_env::ExecutionEnvEvent;
|
||||
use crate::sandbox::SandboxEvent;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let dir = std::env::temp_dir().join(format!("init_event_test_{}", uuid::Uuid::new_v4()));
|
||||
let events: Arc<Mutex<Vec<ExecutionEnvEvent>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let events: Arc<Mutex<Vec<SandboxEvent>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let events_clone = Arc::clone(&events);
|
||||
|
||||
let mut env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let mut env = LocalSandbox::new(dir.clone());
|
||||
env.set_event_callback(Arc::new(move |e| {
|
||||
events_clone.lock().unwrap().push(e);
|
||||
}));
|
||||
|
|
@ -684,10 +684,10 @@ mod tests {
|
|||
let captured = events.lock().unwrap();
|
||||
assert_eq!(captured.len(), 2);
|
||||
assert!(
|
||||
matches!(&captured[0], ExecutionEnvEvent::Initializing { env_type } if env_type == "local")
|
||||
matches!(&captured[0], SandboxEvent::Initializing { provider } if provider == "local")
|
||||
);
|
||||
assert!(
|
||||
matches!(&captured[1], ExecutionEnvEvent::Ready { env_type, .. } if env_type == "local")
|
||||
matches!(&captured[1], SandboxEvent::Ready { provider, .. } if provider == "local")
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
|
|
@ -695,14 +695,14 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn cleanup_emits_events() {
|
||||
use crate::execution_env::ExecutionEnvEvent;
|
||||
use crate::sandbox::SandboxEvent;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let dir = temp_dir();
|
||||
let events: Arc<Mutex<Vec<ExecutionEnvEvent>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let events: Arc<Mutex<Vec<SandboxEvent>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let events_clone = Arc::clone(&events);
|
||||
|
||||
let mut env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let mut env = LocalSandbox::new(dir.clone());
|
||||
env.set_event_callback(Arc::new(move |e| {
|
||||
events_clone.lock().unwrap().push(e);
|
||||
}));
|
||||
|
|
@ -712,10 +712,10 @@ mod tests {
|
|||
let captured = events.lock().unwrap();
|
||||
assert_eq!(captured.len(), 2);
|
||||
assert!(
|
||||
matches!(&captured[0], ExecutionEnvEvent::CleanupStarted { env_type } if env_type == "local")
|
||||
matches!(&captured[0], SandboxEvent::CleanupStarted { provider } if provider == "local")
|
||||
);
|
||||
assert!(
|
||||
matches!(&captured[1], ExecutionEnvEvent::CleanupCompleted { env_type, .. } if env_type == "local")
|
||||
matches!(&captured[1], SandboxEvent::CleanupCompleted { provider, .. } if provider == "local")
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
|
|
@ -730,7 +730,7 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let results = env
|
||||
.grep("println", "test.rs", &GrepOptions::default())
|
||||
.await
|
||||
|
|
@ -746,7 +746,7 @@ mod tests {
|
|||
let dir = temp_dir();
|
||||
std::fs::write(dir.join("test.txt"), "Hello\nhello\nHELLO\n").unwrap();
|
||||
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let results = env
|
||||
.grep(
|
||||
"hello",
|
||||
|
|
@ -768,7 +768,7 @@ mod tests {
|
|||
let dir = temp_dir();
|
||||
std::fs::write(dir.join("test.txt"), "match1\nmatch2\nmatch3\nmatch4\n").unwrap();
|
||||
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let results = env
|
||||
.grep(
|
||||
"match",
|
||||
|
|
@ -792,7 +792,7 @@ mod tests {
|
|||
std::fs::write(dir.join("b.rs"), "").unwrap();
|
||||
std::fs::write(dir.join("c.txt"), "").unwrap();
|
||||
|
||||
let env = LocalExecutionEnvironment::new(dir.clone());
|
||||
let env = LocalSandbox::new(dir.clone());
|
||||
let results = env.glob("*.rs", None).await.unwrap();
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
|
|
|
|||
|
|
@ -83,12 +83,12 @@ mod tests {
|
|||
let tools = make_mcp_tools(Arc::new(mgr));
|
||||
let tool = &tools[0];
|
||||
|
||||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::test_support::MockSandbox;
|
||||
use crate::tool_registry::ToolContext;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({"message": "test message"}),
|
||||
ToolContext {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::config::SessionConfig;
|
||||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::profiles::assemble_system_prompt;
|
||||
use crate::profiles::BaseProfile;
|
||||
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
|
|
@ -71,7 +71,7 @@ impl ProviderProfile for AnthropicProfile {
|
|||
|
||||
fn build_system_prompt(
|
||||
&self,
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env: &dyn Sandbox,
|
||||
env_context: &EnvContext,
|
||||
project_docs: &[String],
|
||||
user_instructions: Option<&str>,
|
||||
|
|
@ -209,7 +209,7 @@ in the project. Keep changes minimal and focused on the task.";
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::test_support::MockSandbox;
|
||||
|
||||
#[test]
|
||||
fn anthropic_profile_identity() {
|
||||
|
|
@ -236,7 +236,7 @@ mod tests {
|
|||
#[test]
|
||||
fn anthropic_system_prompt_contains_env_context() {
|
||||
let profile = AnthropicProfile::new("claude-sonnet-4-20250514");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("You are Claude, an AI coding assistant made by Anthropic"));
|
||||
assert!(prompt.contains("<environment>"));
|
||||
|
|
@ -277,7 +277,7 @@ mod tests {
|
|||
#[test]
|
||||
fn anthropic_system_prompt_includes_project_docs() {
|
||||
let profile = AnthropicProfile::new("claude-sonnet-4-20250514");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let docs = vec!["# Project README".into(), "# CONTRIBUTING guide".into()];
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &docs, None, &[]);
|
||||
assert!(prompt.contains("# Project README"));
|
||||
|
|
@ -287,7 +287,7 @@ mod tests {
|
|||
#[test]
|
||||
fn anthropic_system_prompt_includes_env_context() {
|
||||
let profile = AnthropicProfile::new("claude-opus-4-6");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let ctx = EnvContext {
|
||||
git_branch: Some("feature-branch".into()),
|
||||
is_git_repo: true,
|
||||
|
|
@ -308,7 +308,7 @@ mod tests {
|
|||
#[test]
|
||||
fn anthropic_system_prompt_includes_user_instructions() {
|
||||
let profile = AnthropicProfile::new("claude-opus-4-6");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let ctx = EnvContext::default();
|
||||
let prompt =
|
||||
profile.build_system_prompt(&env, &ctx, &[], Some("Always write tests first"), &[]);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::config::SessionConfig;
|
||||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::profiles::assemble_system_prompt;
|
||||
use crate::profiles::BaseProfile;
|
||||
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
|
|
@ -65,7 +65,7 @@ impl ProviderProfile for GeminiProfile {
|
|||
|
||||
fn build_system_prompt(
|
||||
&self,
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env: &dyn Sandbox,
|
||||
env_context: &EnvContext,
|
||||
project_docs: &[String],
|
||||
user_instructions: Option<&str>,
|
||||
|
|
@ -229,7 +229,7 @@ in the project.";
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::test_support::MockSandbox;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
|
|
@ -251,7 +251,7 @@ mod tests {
|
|||
#[test]
|
||||
fn gemini_system_prompt_contains_identity() {
|
||||
let profile = GeminiProfile::new("gemini-2.0-flash");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("You are Gemini CLI"));
|
||||
assert!(prompt.contains("solving bugs"));
|
||||
|
|
@ -263,7 +263,7 @@ mod tests {
|
|||
#[test]
|
||||
fn gemini_system_prompt_contains_tool_guidance() {
|
||||
let profile = GeminiProfile::new("gemini-2.0-flash");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("read_file"));
|
||||
assert!(prompt.contains("read_many_files"));
|
||||
|
|
@ -281,7 +281,7 @@ mod tests {
|
|||
#[test]
|
||||
fn gemini_system_prompt_contains_project_docs_convention() {
|
||||
let profile = GeminiProfile::new("gemini-2.0-flash");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("GEMINI.md"));
|
||||
assert!(prompt.contains("AGENTS.md"));
|
||||
|
|
@ -290,7 +290,7 @@ mod tests {
|
|||
#[test]
|
||||
fn gemini_system_prompt_contains_coding_best_practices() {
|
||||
let profile = GeminiProfile::new("gemini-2.0-flash");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("clean, maintainable code"));
|
||||
assert!(prompt.contains("Handle errors appropriately"));
|
||||
|
|
@ -300,7 +300,7 @@ mod tests {
|
|||
#[test]
|
||||
fn gemini_system_prompt_contains_env_context() {
|
||||
let profile = GeminiProfile::new("gemini-2.0-flash");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("<environment>"));
|
||||
assert!(prompt.contains("linux"));
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ pub use anthropic::AnthropicProfile;
|
|||
pub use gemini::GeminiProfile;
|
||||
pub use openai::OpenAiProfile;
|
||||
|
||||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::skills::{format_skills_prompt_section, Skill};
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use arc_llm::provider::Provider;
|
||||
|
|
@ -40,7 +40,7 @@ pub struct EnvContext {
|
|||
#[must_use]
|
||||
pub fn assemble_system_prompt(
|
||||
core_prompt: &str,
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env: &dyn Sandbox,
|
||||
env_context: &EnvContext,
|
||||
project_docs: &[String],
|
||||
user_instructions: Option<&str>,
|
||||
|
|
@ -71,12 +71,12 @@ pub fn assemble_system_prompt(
|
|||
|
||||
#[cfg(test)]
|
||||
#[must_use]
|
||||
pub fn build_env_context_block(env: &dyn ExecutionEnvironment) -> String {
|
||||
pub fn build_env_context_block(env: &dyn Sandbox) -> String {
|
||||
build_env_context_block_with(env, &EnvContext::default())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn build_env_context_block_with(env: &dyn ExecutionEnvironment, ctx: &EnvContext) -> String {
|
||||
pub fn build_env_context_block_with(env: &dyn Sandbox, ctx: &EnvContext) -> String {
|
||||
let mut lines = vec![
|
||||
"<environment>".to_string(),
|
||||
format!("Working directory: {}", env.working_directory()),
|
||||
|
|
@ -114,11 +114,11 @@ pub fn build_env_context_block_with(env: &dyn ExecutionEnvironment, ctx: &EnvCon
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::test_support::MockSandbox;
|
||||
|
||||
#[test]
|
||||
fn env_context_block_contains_platform() {
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let block = build_env_context_block(&env);
|
||||
assert!(block.contains("<environment>"));
|
||||
assert!(block.contains("</environment>"));
|
||||
|
|
@ -129,7 +129,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn env_context_block_with_extra_context() {
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let ctx = EnvContext {
|
||||
git_branch: Some("main".into()),
|
||||
is_git_repo: true,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::config::SessionConfig;
|
||||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::profiles::assemble_system_prompt;
|
||||
use crate::profiles::BaseProfile;
|
||||
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
|
|
@ -75,7 +75,7 @@ impl ProviderProfile for OpenAiProfile {
|
|||
|
||||
fn build_system_prompt(
|
||||
&self,
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env: &dyn Sandbox,
|
||||
env_context: &EnvContext,
|
||||
project_docs: &[String],
|
||||
user_instructions: Option<&str>,
|
||||
|
|
@ -210,7 +210,7 @@ in the project.";
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::test_support::MockSandbox;
|
||||
|
||||
#[test]
|
||||
fn openai_profile_identity() {
|
||||
|
|
@ -231,7 +231,7 @@ mod tests {
|
|||
#[test]
|
||||
fn openai_system_prompt_contains_env_context() {
|
||||
let profile = OpenAiProfile::new("o3-mini");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("You are a coding agent powered by OpenAI"));
|
||||
assert!(prompt.contains("<environment>"));
|
||||
|
|
@ -243,7 +243,7 @@ mod tests {
|
|||
#[test]
|
||||
fn openai_system_prompt_contains_tool_guidance() {
|
||||
let profile = OpenAiProfile::new("o3-mini");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("read_file"));
|
||||
assert!(prompt.contains("apply_patch"));
|
||||
|
|
@ -257,7 +257,7 @@ mod tests {
|
|||
#[test]
|
||||
fn openai_system_prompt_contains_coding_best_practices() {
|
||||
let profile = OpenAiProfile::new("o3-mini");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("clean, maintainable code"));
|
||||
assert!(prompt.contains("existing code conventions"));
|
||||
|
|
@ -266,7 +266,7 @@ mod tests {
|
|||
#[test]
|
||||
fn openai_system_prompt_includes_project_docs() {
|
||||
let profile = OpenAiProfile::new("o3-mini");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let docs = vec!["# Project README".into(), "# CONTRIBUTING guide".into()];
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &docs, None, &[]);
|
||||
assert!(prompt.contains("# Project README"));
|
||||
|
|
@ -276,7 +276,7 @@ mod tests {
|
|||
#[test]
|
||||
fn openai_system_prompt_includes_user_instructions() {
|
||||
let profile = OpenAiProfile::new("o3-mini");
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(
|
||||
&env,
|
||||
&EnvContext::default(),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use arc_llm::provider::Provider;
|
||||
|
||||
const BUDGET_BYTES: usize = 32768;
|
||||
|
||||
pub async fn discover_project_docs(
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env: &dyn Sandbox,
|
||||
git_root: &str,
|
||||
working_dir: &str,
|
||||
provider: Provider,
|
||||
|
|
@ -92,8 +92,8 @@ fn truncate_to_budget(content: &str, budget: usize) -> String {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::test_support::MockSandbox;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ mod tests {
|
|||
async fn discovers_agents_md() {
|
||||
let mut files = HashMap::new();
|
||||
files.insert("/repo/AGENTS.md".into(), "Agent instructions".into());
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files,
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -118,7 +118,7 @@ mod tests {
|
|||
files.insert("/repo/.codex/instructions.md".into(), "copilot".into());
|
||||
files.insert("/repo/GEMINI.md".into(), "gemini".into());
|
||||
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files: files.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -128,7 +128,7 @@ mod tests {
|
|||
assert_eq!(anthropic_docs[0], "agents");
|
||||
assert_eq!(anthropic_docs[1], "claude");
|
||||
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files: files.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -138,7 +138,7 @@ mod tests {
|
|||
assert_eq!(openai_docs[0], "agents");
|
||||
assert_eq!(openai_docs[1], "copilot");
|
||||
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files,
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -158,7 +158,7 @@ mod tests {
|
|||
files.insert("/repo/AGENTS.md".into(), large_content.clone());
|
||||
files.insert("/repo/CLAUDE.md".into(), second_content);
|
||||
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files,
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -177,7 +177,7 @@ mod tests {
|
|||
files.insert("/repo/src/AGENTS.md".into(), "src agents".into());
|
||||
files.insert("/repo/src/app/AGENTS.md".into(), "app agents".into());
|
||||
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files,
|
||||
..Default::default()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::profiles::EnvContext;
|
||||
use crate::skills::Skill;
|
||||
use crate::subagent::{
|
||||
|
|
@ -25,7 +25,7 @@ pub trait ProviderProfile: Send + Sync {
|
|||
fn tool_registry_mut(&mut self) -> &mut ToolRegistry;
|
||||
fn build_system_prompt(
|
||||
&self,
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env: &dyn Sandbox,
|
||||
env_context: &EnvContext,
|
||||
project_docs: &[String],
|
||||
user_instructions: Option<&str>,
|
||||
|
|
@ -81,7 +81,7 @@ pub trait ProviderProfile: Send + Sync {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::{MockExecutionEnvironment, TestProfile};
|
||||
use crate::test_support::{MockSandbox, TestProfile};
|
||||
use arc_llm::provider::Provider;
|
||||
|
||||
#[test]
|
||||
|
|
@ -103,7 +103,7 @@ mod tests {
|
|||
#[test]
|
||||
fn profile_build_system_prompt() {
|
||||
let profile = TestProfile::new();
|
||||
let env = MockExecutionEnvironment::linux();
|
||||
let env = MockSandbox::linux();
|
||||
let ctx = EnvContext::default();
|
||||
let docs = vec!["README.md contents".into()];
|
||||
let prompt = profile.build_system_prompt(&env, &ctx, &docs, None, &[]);
|
||||
|
|
@ -113,7 +113,7 @@ mod tests {
|
|||
#[test]
|
||||
fn profile_build_system_prompt_with_user_instructions() {
|
||||
let profile = TestProfile::new();
|
||||
let env = MockExecutionEnvironment::default();
|
||||
let env = MockSandbox::default();
|
||||
let ctx = EnvContext::default();
|
||||
let prompt = profile.build_system_prompt(&env, &ctx, &[], Some("Always use TDD"), &[]);
|
||||
assert!(prompt.contains("Always use TDD"));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::execution_env::*;
|
||||
use crate::sandbox::*;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Component, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
|
@ -8,13 +8,13 @@ use std::sync::{Arc, Mutex};
|
|||
/// Tracks which file paths the agent has seen (via `read_file` or `grep`) and
|
||||
/// returns an error when `write_file` or `delete_file` targets an existing file
|
||||
/// that hasn't been read. Writing to new (non-existent) files is always allowed.
|
||||
pub struct ReadBeforeWriteEnvironment {
|
||||
inner: Arc<dyn ExecutionEnvironment>,
|
||||
pub struct ReadBeforeWriteSandbox {
|
||||
inner: Arc<dyn Sandbox>,
|
||||
read_set: Mutex<HashSet<String>>,
|
||||
}
|
||||
|
||||
impl ReadBeforeWriteEnvironment {
|
||||
pub fn new(inner: Arc<dyn ExecutionEnvironment>) -> Self {
|
||||
impl ReadBeforeWriteSandbox {
|
||||
pub fn new(inner: Arc<dyn Sandbox>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
read_set: Mutex::new(HashSet::new()),
|
||||
|
|
@ -71,8 +71,8 @@ impl ReadBeforeWriteEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
crate::delegate_execution_env! {
|
||||
ReadBeforeWriteEnvironment => inner {
|
||||
crate::delegate_sandbox! {
|
||||
ReadBeforeWriteSandbox => inner {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &str,
|
||||
|
|
@ -116,17 +116,17 @@ crate::delegate_execution_env! {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::test_support::MockSandbox;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Cycle 1: write to existing unread file → error
|
||||
#[tokio::test]
|
||||
async fn write_to_existing_unread_file_returns_error() {
|
||||
let mock = MockExecutionEnvironment {
|
||||
let mock = MockSandbox {
|
||||
files: HashMap::from([("a.ts".into(), "content".into())]),
|
||||
..Default::default()
|
||||
};
|
||||
let env = ReadBeforeWriteEnvironment::new(Arc::new(mock));
|
||||
let env = ReadBeforeWriteSandbox::new(Arc::new(mock));
|
||||
|
||||
let result = env.write_file("a.ts", "new content").await;
|
||||
|
||||
|
|
@ -139,8 +139,8 @@ mod tests {
|
|||
// Cycle 2: write to non-existent file → success
|
||||
#[tokio::test]
|
||||
async fn write_to_nonexistent_file_succeeds() {
|
||||
let mock = MockExecutionEnvironment::default();
|
||||
let env = ReadBeforeWriteEnvironment::new(Arc::new(mock));
|
||||
let mock = MockSandbox::default();
|
||||
let env = ReadBeforeWriteSandbox::new(Arc::new(mock));
|
||||
|
||||
let result = env.write_file("new.ts", "content").await;
|
||||
|
||||
|
|
@ -150,11 +150,11 @@ mod tests {
|
|||
// Cycle 3: read then write → success
|
||||
#[tokio::test]
|
||||
async fn read_then_write_succeeds() {
|
||||
let mock = MockExecutionEnvironment {
|
||||
let mock = MockSandbox {
|
||||
files: HashMap::from([("a.ts".into(), "content".into())]),
|
||||
..Default::default()
|
||||
};
|
||||
let env = ReadBeforeWriteEnvironment::new(Arc::new(mock));
|
||||
let env = ReadBeforeWriteSandbox::new(Arc::new(mock));
|
||||
|
||||
env.read_file("a.ts", None, None).await.unwrap();
|
||||
let result = env.write_file("a.ts", "new content").await;
|
||||
|
|
@ -165,12 +165,12 @@ mod tests {
|
|||
// Cycle 4: grep results populate read set
|
||||
#[tokio::test]
|
||||
async fn grep_populates_read_set() {
|
||||
let mock = MockExecutionEnvironment {
|
||||
let mock = MockSandbox {
|
||||
files: HashMap::from([("b.ts".into(), "content".into())]),
|
||||
grep_results: vec!["b.ts:1:content".into()],
|
||||
..Default::default()
|
||||
};
|
||||
let env = ReadBeforeWriteEnvironment::new(Arc::new(mock));
|
||||
let env = ReadBeforeWriteSandbox::new(Arc::new(mock));
|
||||
|
||||
env.grep("pattern", ".", &GrepOptions::default())
|
||||
.await
|
||||
|
|
@ -183,12 +183,12 @@ mod tests {
|
|||
// Cycle 5: glob does NOT populate read set
|
||||
#[tokio::test]
|
||||
async fn glob_does_not_populate_read_set() {
|
||||
let mock = MockExecutionEnvironment {
|
||||
let mock = MockSandbox {
|
||||
files: HashMap::from([("c.ts".into(), "content".into())]),
|
||||
glob_results: vec!["c.ts".into()],
|
||||
..Default::default()
|
||||
};
|
||||
let env = ReadBeforeWriteEnvironment::new(Arc::new(mock));
|
||||
let env = ReadBeforeWriteSandbox::new(Arc::new(mock));
|
||||
|
||||
env.glob("*.ts", None).await.unwrap();
|
||||
let result = env.write_file("c.ts", "new").await;
|
||||
|
|
@ -199,7 +199,7 @@ mod tests {
|
|||
// Cycle 6: path normalization — relative vs absolute
|
||||
#[tokio::test]
|
||||
async fn path_normalization_relative_and_absolute() {
|
||||
let mock = MockExecutionEnvironment {
|
||||
let mock = MockSandbox {
|
||||
files: HashMap::from([
|
||||
("a.ts".into(), "content".into()),
|
||||
("/work/a.ts".into(), "content".into()),
|
||||
|
|
@ -207,7 +207,7 @@ mod tests {
|
|||
working_dir: "/work",
|
||||
..Default::default()
|
||||
};
|
||||
let env = ReadBeforeWriteEnvironment::new(Arc::new(mock));
|
||||
let env = ReadBeforeWriteSandbox::new(Arc::new(mock));
|
||||
|
||||
env.read_file("a.ts", None, None).await.unwrap();
|
||||
let result = env.write_file("/work/a.ts", "new content").await;
|
||||
|
|
@ -218,11 +218,11 @@ mod tests {
|
|||
// Cycle 7: delete unread file → error
|
||||
#[tokio::test]
|
||||
async fn delete_unread_file_returns_error() {
|
||||
let mock = MockExecutionEnvironment {
|
||||
let mock = MockSandbox {
|
||||
files: HashMap::from([("d.ts".into(), "content".into())]),
|
||||
..Default::default()
|
||||
};
|
||||
let env = ReadBeforeWriteEnvironment::new(Arc::new(mock));
|
||||
let env = ReadBeforeWriteSandbox::new(Arc::new(mock));
|
||||
|
||||
let result = env.delete_file("d.ts").await;
|
||||
|
||||
|
|
@ -232,11 +232,11 @@ mod tests {
|
|||
// Cycle 8: error message is actionable
|
||||
#[tokio::test]
|
||||
async fn error_message_is_actionable() {
|
||||
let mock = MockExecutionEnvironment {
|
||||
let mock = MockSandbox {
|
||||
files: HashMap::from([("main.rs".into(), "fn main() {}".into())]),
|
||||
..Default::default()
|
||||
};
|
||||
let env = ReadBeforeWriteEnvironment::new(Arc::new(mock));
|
||||
let env = ReadBeforeWriteSandbox::new(Arc::new(mock));
|
||||
|
||||
let err = env.write_file("main.rs", "new").await.unwrap_err();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ use std::fmt::Write;
|
|||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// Generates an `#[async_trait] impl ExecutionEnvironment` block for a decorator type
|
||||
/// that wraps an `Arc<dyn ExecutionEnvironment>`. The caller provides custom method
|
||||
/// Generates an `#[async_trait] impl Sandbox` block for a decorator type
|
||||
/// that wraps an `Arc<dyn Sandbox>`. The caller provides custom method
|
||||
/// implementations; all remaining trait methods delegate to the inner field.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// delegate_execution_env! {
|
||||
/// delegate_sandbox! {
|
||||
/// MyDecorator => inner {
|
||||
/// // Only provide methods with custom logic — the rest delegate automatically.
|
||||
/// async fn read_file(&self, path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String, String> {
|
||||
|
|
@ -21,14 +21,14 @@ use tokio_util::sync::CancellationToken;
|
|||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! delegate_execution_env {
|
||||
macro_rules! delegate_sandbox {
|
||||
(
|
||||
$type:ty => $field:ident {
|
||||
$($custom:item)*
|
||||
}
|
||||
) => {
|
||||
#[async_trait::async_trait]
|
||||
impl $crate::execution_env::ExecutionEnvironment for $type {
|
||||
impl $crate::sandbox::Sandbox for $type {
|
||||
$($custom)*
|
||||
|
||||
async fn file_exists(&self, path: &str) -> Result<bool, String> {
|
||||
|
|
@ -39,7 +39,7 @@ macro_rules! delegate_execution_env {
|
|||
&self,
|
||||
path: &str,
|
||||
depth: Option<usize>,
|
||||
) -> Result<Vec<$crate::execution_env::DirEntry>, String> {
|
||||
) -> Result<Vec<$crate::sandbox::DirEntry>, String> {
|
||||
self.$field.list_directory(path, depth).await
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ macro_rules! delegate_execution_env {
|
|||
working_dir: Option<&str>,
|
||||
env_vars: Option<&std::collections::HashMap<String, String>>,
|
||||
cancel_token: Option<tokio_util::sync::CancellationToken>,
|
||||
) -> Result<$crate::execution_env::ExecResult, String> {
|
||||
) -> Result<$crate::sandbox::ExecResult, String> {
|
||||
self.$field
|
||||
.exec_command(command, timeout_ms, working_dir, env_vars, cancel_token)
|
||||
.await
|
||||
|
|
@ -83,40 +83,40 @@ macro_rules! delegate_execution_env {
|
|||
};
|
||||
}
|
||||
|
||||
/// Events emitted during execution environment lifecycle operations.
|
||||
/// Events emitted during sandbox lifecycle operations.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ExecutionEnvEvent {
|
||||
pub enum SandboxEvent {
|
||||
// -- Common lifecycle --
|
||||
Initializing {
|
||||
env_type: String,
|
||||
provider: String,
|
||||
},
|
||||
Ready {
|
||||
env_type: String,
|
||||
provider: String,
|
||||
duration_ms: u64,
|
||||
},
|
||||
InitializeFailed {
|
||||
env_type: String,
|
||||
provider: String,
|
||||
error: String,
|
||||
duration_ms: u64,
|
||||
},
|
||||
CleanupStarted {
|
||||
env_type: String,
|
||||
provider: String,
|
||||
},
|
||||
CleanupCompleted {
|
||||
env_type: String,
|
||||
provider: String,
|
||||
duration_ms: u64,
|
||||
},
|
||||
CleanupFailed {
|
||||
env_type: String,
|
||||
provider: String,
|
||||
error: String,
|
||||
},
|
||||
|
||||
// -- Docker --
|
||||
ImagePulling {
|
||||
image: String,
|
||||
SnapshotPulling {
|
||||
name: String,
|
||||
},
|
||||
ImagePulled {
|
||||
image: String,
|
||||
SnapshotPulled {
|
||||
name: String,
|
||||
duration_ms: u64,
|
||||
},
|
||||
|
||||
|
|
@ -151,43 +151,43 @@ pub enum ExecutionEnvEvent {
|
|||
},
|
||||
}
|
||||
|
||||
impl ExecutionEnvEvent {
|
||||
impl SandboxEvent {
|
||||
pub fn trace(&self) {
|
||||
use tracing::{debug, error, info, warn};
|
||||
match self {
|
||||
Self::Initializing { env_type } => {
|
||||
debug!(env_type, "Execution env initializing");
|
||||
Self::Initializing { provider } => {
|
||||
debug!(provider, "Sandbox initializing");
|
||||
}
|
||||
Self::Ready {
|
||||
env_type,
|
||||
provider,
|
||||
duration_ms,
|
||||
} => {
|
||||
info!(env_type, duration_ms, "Execution env ready");
|
||||
info!(provider, duration_ms, "Sandbox ready");
|
||||
}
|
||||
Self::InitializeFailed {
|
||||
env_type,
|
||||
provider,
|
||||
error,
|
||||
duration_ms,
|
||||
} => {
|
||||
error!(env_type, error, duration_ms, "Execution env init failed");
|
||||
error!(provider, error, duration_ms, "Sandbox init failed");
|
||||
}
|
||||
Self::CleanupStarted { env_type } => {
|
||||
debug!(env_type, "Execution env cleanup started");
|
||||
Self::CleanupStarted { provider } => {
|
||||
debug!(provider, "Sandbox cleanup started");
|
||||
}
|
||||
Self::CleanupCompleted {
|
||||
env_type,
|
||||
provider,
|
||||
duration_ms,
|
||||
} => {
|
||||
debug!(env_type, duration_ms, "Execution env cleanup completed");
|
||||
debug!(provider, duration_ms, "Sandbox cleanup completed");
|
||||
}
|
||||
Self::CleanupFailed { env_type, error } => {
|
||||
warn!(env_type, error, "Execution env cleanup failed");
|
||||
Self::CleanupFailed { provider, error } => {
|
||||
warn!(provider, error, "Sandbox cleanup failed");
|
||||
}
|
||||
Self::ImagePulling { image } => {
|
||||
debug!(image, "Docker image pulling");
|
||||
Self::SnapshotPulling { name } => {
|
||||
debug!(name, "Snapshot pulling");
|
||||
}
|
||||
Self::ImagePulled { image, duration_ms } => {
|
||||
debug!(image, duration_ms, "Docker image pulled");
|
||||
Self::SnapshotPulled { name, duration_ms } => {
|
||||
debug!(name, duration_ms, "Snapshot pulled");
|
||||
}
|
||||
Self::SnapshotEnsuring { name } => {
|
||||
debug!(name, "Snapshot ensuring");
|
||||
|
|
@ -214,8 +214,8 @@ impl ExecutionEnvEvent {
|
|||
}
|
||||
}
|
||||
|
||||
/// Callback type for execution environment events.
|
||||
pub type ExecEnvEventCallback = Arc<dyn Fn(ExecutionEnvEvent) + Send + Sync>;
|
||||
/// Callback type for sandbox events.
|
||||
pub type SandboxEventCallback = Arc<dyn Fn(SandboxEvent) + Send + Sync>;
|
||||
|
||||
/// Formats file content with line numbers for display.
|
||||
///
|
||||
|
|
@ -260,7 +260,7 @@ pub struct GrepOptions {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ExecutionEnvironment: Send + Sync {
|
||||
pub trait Sandbox: Send + Sync {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &str,
|
||||
|
|
@ -300,7 +300,7 @@ pub trait ExecutionEnvironment: Send + Sync {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::test_support::MockSandbox;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -308,7 +308,7 @@ mod tests {
|
|||
async fn mock_env_read_file() {
|
||||
let mut files = HashMap::new();
|
||||
files.insert("test.rs".into(), "hello".into());
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files,
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -318,7 +318,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn mock_env_exec_command() {
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
|
||||
let result = env
|
||||
.exec_command("echo", 5000, None, None, None)
|
||||
.await
|
||||
|
|
@ -329,7 +329,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn mock_env_list_directory() {
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
|
||||
let entries = env.list_directory("/tmp", None).await.unwrap();
|
||||
assert_eq!(entries.len(), 0);
|
||||
}
|
||||
|
|
@ -370,68 +370,68 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn mock_env_platform() {
|
||||
let env = MockExecutionEnvironment::default();
|
||||
let env = MockSandbox::default();
|
||||
assert_eq!(env.platform(), "darwin");
|
||||
assert_eq!(env.working_directory(), "/tmp/test");
|
||||
assert_eq!(env.os_version(), "Darwin 24.0.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_env_event_serialization_round_trip() {
|
||||
fn sandbox_event_serialization_round_trip() {
|
||||
let events = vec![
|
||||
ExecutionEnvEvent::Initializing {
|
||||
env_type: "local".into(),
|
||||
SandboxEvent::Initializing {
|
||||
provider: "local".into(),
|
||||
},
|
||||
ExecutionEnvEvent::Ready {
|
||||
env_type: "local".into(),
|
||||
SandboxEvent::Ready {
|
||||
provider: "local".into(),
|
||||
duration_ms: 50,
|
||||
},
|
||||
ExecutionEnvEvent::InitializeFailed {
|
||||
env_type: "docker".into(),
|
||||
SandboxEvent::InitializeFailed {
|
||||
provider: "docker".into(),
|
||||
error: "no daemon".into(),
|
||||
duration_ms: 100,
|
||||
},
|
||||
ExecutionEnvEvent::CleanupStarted {
|
||||
env_type: "daytona".into(),
|
||||
SandboxEvent::CleanupStarted {
|
||||
provider: "daytona".into(),
|
||||
},
|
||||
ExecutionEnvEvent::CleanupCompleted {
|
||||
env_type: "daytona".into(),
|
||||
SandboxEvent::CleanupCompleted {
|
||||
provider: "daytona".into(),
|
||||
duration_ms: 200,
|
||||
},
|
||||
ExecutionEnvEvent::CleanupFailed {
|
||||
env_type: "docker".into(),
|
||||
SandboxEvent::CleanupFailed {
|
||||
provider: "docker".into(),
|
||||
error: "container gone".into(),
|
||||
},
|
||||
ExecutionEnvEvent::ImagePulling {
|
||||
image: "ubuntu:22.04".into(),
|
||||
SandboxEvent::SnapshotPulling {
|
||||
name: "ubuntu:22.04".into(),
|
||||
},
|
||||
ExecutionEnvEvent::ImagePulled {
|
||||
image: "ubuntu:22.04".into(),
|
||||
SandboxEvent::SnapshotPulled {
|
||||
name: "ubuntu:22.04".into(),
|
||||
duration_ms: 5000,
|
||||
},
|
||||
ExecutionEnvEvent::SnapshotEnsuring {
|
||||
SandboxEvent::SnapshotEnsuring {
|
||||
name: "my-snap".into(),
|
||||
},
|
||||
ExecutionEnvEvent::SnapshotCreating {
|
||||
SandboxEvent::SnapshotCreating {
|
||||
name: "my-snap".into(),
|
||||
},
|
||||
ExecutionEnvEvent::SnapshotReady {
|
||||
SandboxEvent::SnapshotReady {
|
||||
name: "my-snap".into(),
|
||||
duration_ms: 30000,
|
||||
},
|
||||
ExecutionEnvEvent::SnapshotFailed {
|
||||
SandboxEvent::SnapshotFailed {
|
||||
name: "my-snap".into(),
|
||||
error: "build failed".into(),
|
||||
},
|
||||
ExecutionEnvEvent::GitCloneStarted {
|
||||
SandboxEvent::GitCloneStarted {
|
||||
url: "https://github.com/org/repo.git".into(),
|
||||
branch: Some("main".into()),
|
||||
},
|
||||
ExecutionEnvEvent::GitCloneCompleted {
|
||||
SandboxEvent::GitCloneCompleted {
|
||||
url: "https://github.com/org/repo.git".into(),
|
||||
duration_ms: 8000,
|
||||
},
|
||||
ExecutionEnvEvent::GitCloneFailed {
|
||||
SandboxEvent::GitCloneFailed {
|
||||
url: "https://github.com/org/repo.git".into(),
|
||||
error: "auth failed".into(),
|
||||
},
|
||||
|
|
@ -441,17 +441,17 @@ mod tests {
|
|||
|
||||
for event in &events {
|
||||
let json = serde_json::to_string(event).unwrap();
|
||||
let deserialized: ExecutionEnvEvent = serde_json::from_str(&json).unwrap();
|
||||
let deserialized: SandboxEvent = serde_json::from_str(&json).unwrap();
|
||||
let json2 = serde_json::to_string(&deserialized).unwrap();
|
||||
assert_eq!(json, json2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_env_event_callback_type_compiles() {
|
||||
let cb: ExecEnvEventCallback = Arc::new(|_event| {});
|
||||
cb(ExecutionEnvEvent::Initializing {
|
||||
env_type: "test".into(),
|
||||
fn sandbox_event_callback_type_compiles() {
|
||||
let cb: SandboxEventCallback = Arc::new(|_event| {});
|
||||
cb(SandboxEvent::Initializing {
|
||||
provider: "test".into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::config::SessionConfig;
|
||||
use crate::error::AgentError;
|
||||
use crate::event::EventEmitter;
|
||||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::file_tracker::FileTracker;
|
||||
use crate::history::History;
|
||||
use crate::loop_detection::detect_loop;
|
||||
|
|
@ -31,7 +31,7 @@ pub struct Session {
|
|||
state: SessionState,
|
||||
llm_client: Client,
|
||||
provider_profile: Arc<dyn ProviderProfile>,
|
||||
execution_env: Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
steering_queue: Arc<Mutex<VecDeque<String>>>,
|
||||
followup_queue: Arc<Mutex<VecDeque<String>>>,
|
||||
cancel_token: CancellationToken,
|
||||
|
|
@ -47,7 +47,7 @@ impl Session {
|
|||
pub fn new(
|
||||
llm_client: Client,
|
||||
provider_profile: Arc<dyn ProviderProfile>,
|
||||
execution_env: Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
config: SessionConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -58,7 +58,7 @@ impl Session {
|
|||
state: SessionState::Idle,
|
||||
llm_client,
|
||||
provider_profile,
|
||||
execution_env,
|
||||
sandbox,
|
||||
steering_queue: Arc::new(Mutex::new(VecDeque::new())),
|
||||
followup_queue: Arc::new(Mutex::new(VecDeque::new())),
|
||||
cancel_token: CancellationToken::new(),
|
||||
|
|
@ -80,11 +80,11 @@ impl Session {
|
|||
.config
|
||||
.git_root
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.execution_env.working_directory().to_string());
|
||||
.unwrap_or_else(|| self.sandbox.working_directory().to_string());
|
||||
self.project_docs = discover_project_docs(
|
||||
self.execution_env.as_ref(),
|
||||
self.sandbox.as_ref(),
|
||||
&doc_root,
|
||||
self.execution_env.working_directory(),
|
||||
self.sandbox.working_directory(),
|
||||
self.provider_profile.provider(),
|
||||
)
|
||||
.await;
|
||||
|
|
@ -98,7 +98,7 @@ impl Session {
|
|||
default_skill_dirs(home.as_deref(), self.config.git_root.as_deref())
|
||||
}
|
||||
};
|
||||
self.skills = discover_skills(self.execution_env.as_ref(), &skill_dirs).await;
|
||||
self.skills = discover_skills(self.sandbox.as_ref(), &skill_dirs).await;
|
||||
debug!(skill_count = self.skills.len(), "Skills discovered");
|
||||
|
||||
// Register use_skill tool when skills are available
|
||||
|
|
@ -158,7 +158,7 @@ impl Session {
|
|||
|
||||
// Build system prompt once (static for the session lifetime)
|
||||
self.system_prompt = self.provider_profile.build_system_prompt(
|
||||
self.execution_env.as_ref(),
|
||||
self.sandbox.as_ref(),
|
||||
&self.env_context,
|
||||
&self.project_docs,
|
||||
self.config.user_instructions.as_deref(),
|
||||
|
|
@ -172,7 +172,7 @@ impl Session {
|
|||
|
||||
// Detect git info via execution environment
|
||||
let git_branch = self
|
||||
.execution_env
|
||||
.sandbox
|
||||
.exec_command("git rev-parse --abbrev-ref HEAD", 5000, None, None, None)
|
||||
.await
|
||||
.ok()
|
||||
|
|
@ -182,7 +182,7 @@ impl Session {
|
|||
let is_git_repo = git_branch.is_some();
|
||||
|
||||
let git_status_short = if is_git_repo {
|
||||
self.execution_env
|
||||
self.sandbox
|
||||
.exec_command("git status --short", 5000, None, None, None)
|
||||
.await
|
||||
.ok()
|
||||
|
|
@ -194,7 +194,7 @@ impl Session {
|
|||
};
|
||||
|
||||
let git_recent_commits = if is_git_repo {
|
||||
self.execution_env
|
||||
self.sandbox
|
||||
.exec_command("git log --oneline -10", 5000, None, None, None)
|
||||
.await
|
||||
.ok()
|
||||
|
|
@ -585,7 +585,7 @@ impl Session {
|
|||
&tool_calls,
|
||||
self.provider_profile.supports_parallel_tool_calls(),
|
||||
self.provider_profile.tool_registry(),
|
||||
self.execution_env.clone(),
|
||||
self.sandbox.clone(),
|
||||
self.config.tool_approval.as_ref(),
|
||||
&self.cancel_token,
|
||||
&self.config,
|
||||
|
|
@ -1056,7 +1056,7 @@ mod tests {
|
|||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let profile = Arc::new(TestProfile::with_tools(registry));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
enable_loop_detection: false,
|
||||
..Default::default()
|
||||
|
|
@ -1091,7 +1091,7 @@ mod tests {
|
|||
});
|
||||
let client = make_client(error_provider).await;
|
||||
let profile = Arc::new(TestProfile::new());
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, SessionConfig::default());
|
||||
|
||||
let result = session.process_input("Hello").await;
|
||||
|
|
@ -1170,7 +1170,7 @@ mod tests {
|
|||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let profile = Arc::new(TestProfile::parallel(registry));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, SessionConfig::default());
|
||||
let mut rx = session.subscribe();
|
||||
|
||||
|
|
@ -1221,7 +1221,7 @@ mod tests {
|
|||
let client = make_client(provider).await;
|
||||
let registry = ToolRegistry::new();
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, SessionConfig::default());
|
||||
let mut rx = session.subscribe();
|
||||
|
||||
|
|
@ -1247,7 +1247,7 @@ mod tests {
|
|||
let provider_ref = provider.clone();
|
||||
let client = make_client(provider as Arc<dyn ProviderAdapter>).await;
|
||||
let profile = Arc::new(TestProfile::new());
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, SessionConfig::default());
|
||||
|
||||
// Default reasoning_effort is None
|
||||
|
|
@ -1270,7 +1270,7 @@ mod tests {
|
|||
let registry = ToolRegistry::new();
|
||||
// Large context window so short input stays well under 80%
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 200_000));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, SessionConfig::default());
|
||||
let mut rx = session.subscribe();
|
||||
|
||||
|
|
@ -1399,7 +1399,7 @@ mod tests {
|
|||
let provider_ref = provider.clone();
|
||||
let client = make_client(provider as Arc<dyn ProviderAdapter>).await;
|
||||
let profile = Arc::new(TestProfile::new());
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
user_instructions: Some("Always use TDD".into()),
|
||||
..Default::default()
|
||||
|
|
@ -1621,7 +1621,7 @@ mod tests {
|
|||
});
|
||||
let client = make_client(provider as Arc<dyn ProviderAdapter>).await;
|
||||
let profile = Arc::new(TestProfile::new());
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, SessionConfig::default());
|
||||
|
||||
let result = session.process_input("Hello").await;
|
||||
|
|
@ -1647,7 +1647,7 @@ mod tests {
|
|||
let client = make_client(provider).await;
|
||||
let registry = ToolRegistry::new();
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
enable_context_compaction: true,
|
||||
compaction_preserve_turns: 1,
|
||||
|
|
@ -1690,7 +1690,7 @@ mod tests {
|
|||
let client = make_client(provider).await;
|
||||
let registry = ToolRegistry::new();
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
enable_context_compaction: false,
|
||||
..Default::default()
|
||||
|
|
@ -1773,7 +1773,7 @@ mod tests {
|
|||
let client = make_client(provider as Arc<dyn ProviderAdapter>).await;
|
||||
let registry = ToolRegistry::new();
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
enable_context_compaction: true,
|
||||
compaction_preserve_turns: 1,
|
||||
|
|
@ -1876,7 +1876,7 @@ mod tests {
|
|||
let client = make_client(provider.clone() as Arc<dyn ProviderAdapter>).await;
|
||||
// Tiny context window to force compaction
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
enable_context_compaction: true,
|
||||
compaction_preserve_turns: 1,
|
||||
|
|
@ -1977,8 +1977,8 @@ mod tests {
|
|||
let client = make_client(provider).await;
|
||||
let profile: Arc<dyn crate::provider_profile::ProviderProfile> =
|
||||
Arc::new(TestProfile::new());
|
||||
let env: Arc<dyn crate::execution_env::ExecutionEnvironment> =
|
||||
Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn crate::sandbox::Sandbox> =
|
||||
Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, config);
|
||||
|
||||
// Subscribe to events before initialize
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::tool_registry::RegisteredTool;
|
||||
use crate::tools::required_str;
|
||||
use arc_llm::types::ToolDefinition;
|
||||
|
|
@ -220,7 +220,7 @@ pub fn default_skill_dirs(home_dir: Option<&str>, git_root: Option<&str>) -> Vec
|
|||
dirs
|
||||
}
|
||||
|
||||
pub async fn discover_skills(env: &dyn ExecutionEnvironment, dirs: &[String]) -> Vec<Skill> {
|
||||
pub async fn discover_skills(env: &dyn Sandbox, dirs: &[String]) -> Vec<Skill> {
|
||||
let mut skills_by_name: std::collections::HashMap<String, Skill> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
|
|
@ -253,7 +253,7 @@ pub async fn discover_skills(env: &dyn ExecutionEnvironment, dirs: &[String]) ->
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::test_support::MockSandbox;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// --- parse_skill tests ---
|
||||
|
|
@ -444,7 +444,7 @@ name: trimmed
|
|||
"/skills/commit/SKILL.md".into(),
|
||||
"---\nname: commit\ndescription: Make a commit\n---\nDo commit".into(),
|
||||
);
|
||||
let env = MockExecutionEnvironment {
|
||||
let env = MockSandbox {
|
||||
files,
|
||||
glob_results: vec!["/skills/commit/SKILL.md".into()],
|
||||
..Default::default()
|
||||
|
|
@ -464,7 +464,7 @@ name: trimmed
|
|||
"---\nname: good\n---\nGood template".into(),
|
||||
);
|
||||
files.insert("/skills/bad/SKILL.md".into(), "no frontmatter here".into());
|
||||
let env = MockExecutionEnvironment {
|
||||
let env = MockSandbox {
|
||||
files,
|
||||
glob_results: vec![
|
||||
"/skills/good/SKILL.md".into(),
|
||||
|
|
@ -480,7 +480,7 @@ name: trimmed
|
|||
|
||||
#[tokio::test]
|
||||
async fn discover_empty_dirs() {
|
||||
let env = MockExecutionEnvironment::default();
|
||||
let env = MockSandbox::default();
|
||||
let skills = discover_skills(&env, &[]).await;
|
||||
assert!(skills.is_empty());
|
||||
}
|
||||
|
|
@ -497,10 +497,10 @@ name: trimmed
|
|||
"---\nname: commit\ndescription: Project commit\n---\nProject template".into(),
|
||||
);
|
||||
|
||||
// We need separate envs because MockExecutionEnvironment returns the same glob_results
|
||||
// We need separate envs because MockSandbox returns the same glob_results
|
||||
// for all calls. Instead, we test with a single env that has both files
|
||||
// and glob returns both — the later dir overrides the earlier.
|
||||
let env = MockExecutionEnvironment {
|
||||
let env = MockSandbox {
|
||||
files,
|
||||
glob_results: vec![
|
||||
"/global/commit/SKILL.md".into(),
|
||||
|
|
@ -543,8 +543,8 @@ name: trimmed
|
|||
let skills = Arc::new(test_skills());
|
||||
let tool = make_use_skill_tool(skills);
|
||||
|
||||
let env: Arc<dyn crate::execution_env::ExecutionEnvironment> =
|
||||
Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn crate::sandbox::Sandbox> =
|
||||
Arc::new(MockSandbox::default());
|
||||
let args = serde_json::json!({"skill_name": "commit"});
|
||||
let ctx = crate::tool_registry::ToolContext {
|
||||
env,
|
||||
|
|
@ -562,8 +562,8 @@ name: trimmed
|
|||
let skills = Arc::new(test_skills());
|
||||
let tool = make_use_skill_tool(skills);
|
||||
|
||||
let env: Arc<dyn crate::execution_env::ExecutionEnvironment> =
|
||||
Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn crate::sandbox::Sandbox> =
|
||||
Arc::new(MockSandbox::default());
|
||||
let args = serde_json::json!({"skill_name": "nonexistent"});
|
||||
let ctx = crate::tool_registry::ToolContext {
|
||||
env,
|
||||
|
|
@ -579,8 +579,8 @@ name: trimmed
|
|||
let skills = Arc::new(test_skills());
|
||||
let tool = make_use_skill_tool(skills);
|
||||
|
||||
let env: Arc<dyn crate::execution_env::ExecutionEnvironment> =
|
||||
Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn crate::sandbox::Sandbox> =
|
||||
Arc::new(MockSandbox::default());
|
||||
let args = serde_json::json!({});
|
||||
let ctx = crate::tool_registry::ToolContext {
|
||||
env,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::config::SessionConfig;
|
||||
use crate::execution_env::*;
|
||||
use crate::sandbox::*;
|
||||
use crate::profiles::EnvContext;
|
||||
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
use crate::session::Session;
|
||||
|
|
@ -15,9 +15,9 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||
use std::sync::{Arc, Mutex};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
// --- MockExecutionEnvironment ---
|
||||
// --- MockSandbox ---
|
||||
|
||||
pub struct MockExecutionEnvironment {
|
||||
pub struct MockSandbox {
|
||||
pub files: HashMap<String, String>,
|
||||
pub exec_result: ExecResult,
|
||||
pub grep_results: Vec<String>,
|
||||
|
|
@ -33,10 +33,10 @@ pub struct MockExecutionEnvironment {
|
|||
pub captured_timeout: Mutex<Option<u64>>,
|
||||
/// Captures the `command` argument from `exec_command` calls.
|
||||
pub captured_command: Mutex<Option<String>>,
|
||||
pub event_callback: Option<crate::execution_env::ExecEnvEventCallback>,
|
||||
pub event_callback: Option<crate::sandbox::SandboxEventCallback>,
|
||||
}
|
||||
|
||||
impl MockExecutionEnvironment {
|
||||
impl MockSandbox {
|
||||
pub fn linux() -> Self {
|
||||
Self {
|
||||
working_dir: "/home/test",
|
||||
|
|
@ -47,8 +47,8 @@ impl MockExecutionEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
impl MockExecutionEnvironment {
|
||||
fn emit(&self, event: crate::execution_env::ExecutionEnvEvent) {
|
||||
impl MockSandbox {
|
||||
fn emit(&self, event: crate::sandbox::SandboxEvent) {
|
||||
event.trace();
|
||||
if let Some(ref cb) = self.event_callback {
|
||||
cb(event);
|
||||
|
|
@ -56,7 +56,7 @@ impl MockExecutionEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for MockExecutionEnvironment {
|
||||
impl Default for MockSandbox {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
files: HashMap::new(),
|
||||
|
|
@ -82,7 +82,7 @@ impl Default for MockExecutionEnvironment {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionEnvironment for MockExecutionEnvironment {
|
||||
impl Sandbox for MockSandbox {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &str,
|
||||
|
|
@ -163,22 +163,22 @@ impl ExecutionEnvironment for MockExecutionEnvironment {
|
|||
}
|
||||
|
||||
async fn initialize(&self) -> Result<(), String> {
|
||||
self.emit(crate::execution_env::ExecutionEnvEvent::Initializing {
|
||||
env_type: "mock".into(),
|
||||
self.emit(crate::sandbox::SandboxEvent::Initializing {
|
||||
provider: "mock".into(),
|
||||
});
|
||||
self.emit(crate::execution_env::ExecutionEnvEvent::Ready {
|
||||
env_type: "mock".into(),
|
||||
self.emit(crate::sandbox::SandboxEvent::Ready {
|
||||
provider: "mock".into(),
|
||||
duration_ms: 0,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cleanup(&self) -> Result<(), String> {
|
||||
self.emit(crate::execution_env::ExecutionEnvEvent::CleanupStarted {
|
||||
env_type: "mock".into(),
|
||||
self.emit(crate::sandbox::SandboxEvent::CleanupStarted {
|
||||
provider: "mock".into(),
|
||||
});
|
||||
self.emit(crate::execution_env::ExecutionEnvEvent::CleanupCompleted {
|
||||
env_type: "mock".into(),
|
||||
self.emit(crate::sandbox::SandboxEvent::CleanupCompleted {
|
||||
provider: "mock".into(),
|
||||
duration_ms: 0,
|
||||
});
|
||||
Ok(())
|
||||
|
|
@ -197,15 +197,15 @@ impl ExecutionEnvironment for MockExecutionEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
// --- MutableMockExecutionEnvironment ---
|
||||
// --- MutableMockSandbox ---
|
||||
|
||||
/// A mock execution environment with Mutex-protected files for tests that need
|
||||
/// write operations to be visible to subsequent reads (e.g., `apply_patch` tests).
|
||||
pub struct MutableMockExecutionEnvironment {
|
||||
pub struct MutableMockSandbox {
|
||||
pub files: Mutex<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl MutableMockExecutionEnvironment {
|
||||
impl MutableMockSandbox {
|
||||
pub fn new(files: HashMap<String, String>) -> Self {
|
||||
Self {
|
||||
files: Mutex::new(files),
|
||||
|
|
@ -214,7 +214,7 @@ impl MutableMockExecutionEnvironment {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionEnvironment for MutableMockExecutionEnvironment {
|
||||
impl Sandbox for MutableMockSandbox {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &str,
|
||||
|
|
@ -370,7 +370,7 @@ impl ProviderProfile for TestProfile {
|
|||
|
||||
fn build_system_prompt(
|
||||
&self,
|
||||
_env: &dyn ExecutionEnvironment,
|
||||
_env: &dyn Sandbox,
|
||||
_env_context: &EnvContext,
|
||||
_project_docs: &[String],
|
||||
user_instructions: Option<&str>,
|
||||
|
|
@ -508,7 +508,7 @@ pub async fn make_session(responses: Vec<Response>) -> Session {
|
|||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let profile = Arc::new(TestProfile::new());
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
Session::new(client, profile, env, SessionConfig::default())
|
||||
}
|
||||
|
||||
|
|
@ -516,7 +516,7 @@ pub async fn make_session_with_tools(responses: Vec<Response>, registry: ToolReg
|
|||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let profile = Arc::new(TestProfile::with_tools(registry));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
Session::new(client, profile, env, SessionConfig::default())
|
||||
}
|
||||
|
||||
|
|
@ -524,7 +524,7 @@ pub async fn make_session_with_config(responses: Vec<Response>, config: SessionC
|
|||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let profile = Arc::new(TestProfile::new());
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
Session::new(client, profile, env, config)
|
||||
}
|
||||
|
||||
|
|
@ -536,7 +536,7 @@ pub async fn make_session_with_tools_and_config(
|
|||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let profile = Arc::new(TestProfile::with_tools(registry));
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
Session::new(client, profile, env, config)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::config::{SessionConfig, ToolApprovalFn};
|
||||
use crate::event::EventEmitter;
|
||||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use crate::truncation::truncate_tool_output;
|
||||
use crate::types::AgentEvent;
|
||||
|
|
@ -14,7 +14,7 @@ pub async fn execute_tool_calls(
|
|||
tool_calls: &[arc_llm::types::ToolCall],
|
||||
parallel: bool,
|
||||
registry: &ToolRegistry,
|
||||
env: Arc<dyn ExecutionEnvironment>,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_approval: Option<&ToolApprovalFn>,
|
||||
cancel_token: &CancellationToken,
|
||||
config: &SessionConfig,
|
||||
|
|
@ -52,7 +52,7 @@ pub async fn execute_tool_calls(
|
|||
async fn execute_tool_calls_sequential(
|
||||
tool_calls: &[arc_llm::types::ToolCall],
|
||||
registry: &ToolRegistry,
|
||||
env: Arc<dyn ExecutionEnvironment>,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_approval: Option<&ToolApprovalFn>,
|
||||
cancel_token: &CancellationToken,
|
||||
config: &SessionConfig,
|
||||
|
|
@ -86,7 +86,7 @@ async fn execute_tool_calls_sequential(
|
|||
async fn execute_tool_calls_parallel(
|
||||
tool_calls: &[arc_llm::types::ToolCall],
|
||||
registry: &ToolRegistry,
|
||||
env: Arc<dyn ExecutionEnvironment>,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_approval: Option<&ToolApprovalFn>,
|
||||
cancel_token: &CancellationToken,
|
||||
config: &SessionConfig,
|
||||
|
|
@ -129,7 +129,7 @@ async fn execute_tool_calls_parallel(
|
|||
pub async fn execute_and_emit_one_tool(
|
||||
tc: &arc_llm::types::ToolCall,
|
||||
registry: &ToolRegistry,
|
||||
env: Arc<dyn ExecutionEnvironment>,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_approval: Option<&ToolApprovalFn>,
|
||||
cancel_token: CancellationToken,
|
||||
config: &SessionConfig,
|
||||
|
|
@ -154,7 +154,7 @@ pub async fn execute_and_emit_one_tool(
|
|||
async fn execute_and_emit_one_tool_with_lookup(
|
||||
tc: &arc_llm::types::ToolCall,
|
||||
registered_tool: Option<&crate::tool_registry::RegisteredTool>,
|
||||
env: Arc<dyn ExecutionEnvironment>,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_approval: Option<&ToolApprovalFn>,
|
||||
cancel_token: CancellationToken,
|
||||
config: &SessionConfig,
|
||||
|
|
@ -207,7 +207,7 @@ async fn execute_one_tool(
|
|||
tool_name: &str,
|
||||
arguments: &serde_json::Value,
|
||||
registered_tool: Option<&crate::tool_registry::RegisteredTool>,
|
||||
env: Arc<dyn ExecutionEnvironment>,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_approval: Option<&ToolApprovalFn>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> ToolResult {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use arc_llm::types::ToolDefinition;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
|
|
@ -7,7 +7,7 @@ use std::sync::Arc;
|
|||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub struct ToolContext {
|
||||
pub env: Arc<dyn ExecutionEnvironment>,
|
||||
pub env: Arc<dyn Sandbox>,
|
||||
pub cancel: CancellationToken,
|
||||
}
|
||||
|
||||
|
|
@ -171,10 +171,10 @@ mod tests {
|
|||
let tool = registry.get("echo").unwrap();
|
||||
|
||||
use super::ToolContext;
|
||||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::test_support::MockSandbox;
|
||||
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
|
||||
let ctx = ToolContext {
|
||||
env,
|
||||
cancel: CancellationToken::new(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::config::SessionConfig;
|
||||
use crate::execution_env::GrepOptions;
|
||||
use crate::sandbox::GrepOptions;
|
||||
use crate::tool_registry::RegisteredTool;
|
||||
use arc_llm::client::Client;
|
||||
use arc_llm::provider::ModelId;
|
||||
|
|
@ -590,8 +590,8 @@ pub(crate) fn make_web_fetch_tool(summarizer: Option<WebFetchSummarizer>) -> Reg
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::execution_env::*;
|
||||
use crate::test_support::MockExecutionEnvironment;
|
||||
use crate::sandbox::*;
|
||||
use crate::test_support::MockSandbox;
|
||||
use crate::tool_registry::ToolContext;
|
||||
use std::collections::HashMap;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
|
@ -601,7 +601,7 @@ mod tests {
|
|||
let tool = make_read_file_tool();
|
||||
let mut files = HashMap::new();
|
||||
files.insert("/test.txt".into(), " 1 | hello\n 2 | world".into());
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files,
|
||||
apply_read_offset_limit: true,
|
||||
..Default::default()
|
||||
|
|
@ -625,7 +625,7 @@ mod tests {
|
|||
"/test.txt".into(),
|
||||
" 1 | line1\n 2 | line2\n 3 | line3\n 4 | line4".into(),
|
||||
);
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files,
|
||||
apply_read_offset_limit: true,
|
||||
..Default::default()
|
||||
|
|
@ -644,8 +644,8 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn write_file_calls_env() {
|
||||
let tool = make_write_file_tool();
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env_clone: Arc<dyn ExecutionEnvironment> = env.clone();
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let env_clone: Arc<dyn Sandbox> = env.clone();
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({"file_path": "/out.txt", "content": "hello"}),
|
||||
ToolContext {
|
||||
|
|
@ -666,11 +666,11 @@ mod tests {
|
|||
let tool = make_edit_file_tool();
|
||||
let mut files = HashMap::new();
|
||||
files.insert("/f.txt".into(), " 1 | hello world".into());
|
||||
let env = Arc::new(MockExecutionEnvironment {
|
||||
let env = Arc::new(MockSandbox {
|
||||
files,
|
||||
..Default::default()
|
||||
});
|
||||
let env_clone: Arc<dyn ExecutionEnvironment> = env.clone();
|
||||
let env_clone: Arc<dyn Sandbox> = env.clone();
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({
|
||||
"file_path": "/f.txt",
|
||||
|
|
@ -694,7 +694,7 @@ mod tests {
|
|||
let tool = make_edit_file_tool();
|
||||
let mut files = HashMap::new();
|
||||
files.insert("/f.txt".into(), " 1 | hello world".into());
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files,
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -718,7 +718,7 @@ mod tests {
|
|||
let tool = make_edit_file_tool();
|
||||
let mut files = HashMap::new();
|
||||
files.insert("/f.txt".into(), " 1 | aa bb aa".into());
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
files,
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -744,11 +744,11 @@ mod tests {
|
|||
let tool = make_edit_file_tool();
|
||||
let mut files = HashMap::new();
|
||||
files.insert("/f.txt".into(), " 1 | aa bb aa".into());
|
||||
let env = Arc::new(MockExecutionEnvironment {
|
||||
let env = Arc::new(MockSandbox {
|
||||
files,
|
||||
..Default::default()
|
||||
});
|
||||
let env_clone: Arc<dyn ExecutionEnvironment> = env.clone();
|
||||
let env_clone: Arc<dyn Sandbox> = env.clone();
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({
|
||||
"file_path": "/f.txt",
|
||||
|
|
@ -771,7 +771,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn shell_basic_command() {
|
||||
let tool = make_shell_tool();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: "hello".into(),
|
||||
stderr: String::new(),
|
||||
|
|
@ -797,8 +797,8 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn shell_with_timeout() {
|
||||
let tool = make_shell_tool();
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env_clone: Arc<dyn ExecutionEnvironment> = env.clone();
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let env_clone: Arc<dyn Sandbox> = env.clone();
|
||||
let _result = (tool.executor)(
|
||||
serde_json::json!({"command": "sleep 1", "timeout_ms": 5000}),
|
||||
ToolContext {
|
||||
|
|
@ -813,7 +813,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn shell_nonzero_exit_code() {
|
||||
let tool = make_shell_tool();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: String::new(),
|
||||
stderr: "error".into(),
|
||||
|
|
@ -839,7 +839,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn shell_timeout_output() {
|
||||
let tool = make_shell_tool();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
|
|
@ -864,7 +864,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn grep_basic() {
|
||||
let tool = make_grep_tool();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
grep_results: vec![
|
||||
"src/main.rs:10:fn main()".into(),
|
||||
"src/lib.rs:5:pub fn".into(),
|
||||
|
|
@ -887,7 +887,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn glob_basic() {
|
||||
let tool = make_glob_tool();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
glob_results: vec!["src/main.rs".into(), "src/lib.rs".into()],
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -907,7 +907,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn web_search_missing_api_key_returns_error() {
|
||||
let tool = make_web_search_tool_with_api_key(None);
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({"query": "test"}),
|
||||
ToolContext {
|
||||
|
|
@ -926,7 +926,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn web_search_missing_query_returns_error() {
|
||||
let tool = make_web_search_tool_with_api_key(Some("fake-key".into()));
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({}),
|
||||
ToolContext {
|
||||
|
|
@ -968,7 +968,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn web_fetch_builds_curl_command() {
|
||||
let tool = make_web_fetch_tool(None);
|
||||
let env = Arc::new(MockExecutionEnvironment {
|
||||
let env = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: "<html><body><h1>hello</h1></body></html>".into(),
|
||||
stderr: String::new(),
|
||||
|
|
@ -978,7 +978,7 @@ mod tests {
|
|||
},
|
||||
..Default::default()
|
||||
});
|
||||
let env_clone: Arc<dyn ExecutionEnvironment> = env.clone();
|
||||
let env_clone: Arc<dyn Sandbox> = env.clone();
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({"url": "https://example.com"}),
|
||||
ToolContext {
|
||||
|
|
@ -1014,7 +1014,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn web_fetch_rejects_non_http_url() {
|
||||
let tool = make_web_fetch_tool(None);
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({"url": "ftp://example.com/file"}),
|
||||
ToolContext {
|
||||
|
|
@ -1033,8 +1033,8 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn web_fetch_timeout_flows_through() {
|
||||
let tool = make_web_fetch_tool(None);
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env_clone: Arc<dyn ExecutionEnvironment> = env.clone();
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let env_clone: Arc<dyn Sandbox> = env.clone();
|
||||
let _result = (tool.executor)(
|
||||
serde_json::json!({"url": "https://example.com", "timeout_ms": 15000}),
|
||||
ToolContext {
|
||||
|
|
@ -1054,8 +1054,8 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn web_fetch_timeout_capped_at_60s() {
|
||||
let tool = make_web_fetch_tool(None);
|
||||
let env = Arc::new(MockExecutionEnvironment::default());
|
||||
let env_clone: Arc<dyn ExecutionEnvironment> = env.clone();
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let env_clone: Arc<dyn Sandbox> = env.clone();
|
||||
let _result = (tool.executor)(
|
||||
serde_json::json!({"url": "https://example.com", "timeout_ms": 120000}),
|
||||
ToolContext {
|
||||
|
|
@ -1076,7 +1076,7 @@ mod tests {
|
|||
async fn web_fetch_truncates_large_output() {
|
||||
let large_content = "x".repeat(150 * 1024);
|
||||
let tool = make_web_fetch_tool(None);
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: large_content,
|
||||
stderr: String::new(),
|
||||
|
|
@ -1102,7 +1102,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn web_fetch_returns_error_on_nonzero_exit() {
|
||||
let tool = make_web_fetch_tool(None);
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: String::new(),
|
||||
stderr: "curl: (6) Could not resolve host".into(),
|
||||
|
|
@ -1145,7 +1145,7 @@ mod tests {
|
|||
};
|
||||
|
||||
let tool = make_web_fetch_tool(Some(summarizer));
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: "<html><body><p>Lots of content about Rust...</p></body></html>".into(),
|
||||
stderr: String::new(),
|
||||
|
|
@ -1173,7 +1173,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn web_fetch_prompt_without_summarizer_returns_content_with_note() {
|
||||
let tool = make_web_fetch_tool(None);
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: "<html><body><p>Rust is a systems programming language.</p></body></html>"
|
||||
.into(),
|
||||
|
|
@ -1236,7 +1236,7 @@ mod tests {
|
|||
};
|
||||
|
||||
let tool = make_web_fetch_tool(Some(summarizer));
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: "<html><body><p>Page content</p></body></html>".into(),
|
||||
stderr: String::new(),
|
||||
|
|
@ -1295,7 +1295,7 @@ mod tests {
|
|||
let api_key = std::env::var("BRAVE_SEARCH_API_KEY")
|
||||
.expect("BRAVE_SEARCH_API_KEY must be set to run this test");
|
||||
let tool = make_web_search_tool_with_api_key(Some(api_key));
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({"query": "rust programming language"}),
|
||||
ToolContext {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::tool_registry::RegisteredTool;
|
||||
use arc_llm::types::ToolDefinition;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -130,7 +130,7 @@ pub fn parse_v4a_patch(text: &str) -> Result<Vec<PatchOperation>, String> {
|
|||
/// Returns an error if any file operation fails.
|
||||
pub async fn apply_patch_operations(
|
||||
ops: &[PatchOperation],
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env: &dyn Sandbox,
|
||||
) -> Result<String, String> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
|
|
@ -244,7 +244,7 @@ pub fn make_apply_patch_tool() -> RegisteredTool {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::MutableMockExecutionEnvironment;
|
||||
use crate::test_support::MutableMockSandbox;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
|
|
@ -338,7 +338,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn apply_patch_add_file() {
|
||||
let env = MutableMockExecutionEnvironment::new(HashMap::new());
|
||||
let env = MutableMockSandbox::new(HashMap::new());
|
||||
let ops = vec![PatchOperation::Add {
|
||||
path: "src/new.rs".into(),
|
||||
content: "fn new() {}".into(),
|
||||
|
|
@ -358,7 +358,7 @@ mod tests {
|
|||
"src/lib.rs".to_string(),
|
||||
"fn hello() {\n println!(\"old\");\n}".to_string(),
|
||||
);
|
||||
let env = MutableMockExecutionEnvironment::new(files);
|
||||
let env = MutableMockSandbox::new(files);
|
||||
|
||||
let ops = vec![PatchOperation::Update {
|
||||
path: "src/lib.rs".into(),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::path::Path;
|
|||
use std::sync::Arc;
|
||||
|
||||
use arc_agent::{
|
||||
AnthropicProfile, GeminiProfile, LocalExecutionEnvironment, OpenAiProfile, ProviderProfile,
|
||||
AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, ProviderProfile,
|
||||
Session, SessionConfig, SubAgentManager, WebFetchSummarizer,
|
||||
};
|
||||
use arc_llm::client::Client;
|
||||
|
|
@ -43,7 +43,7 @@ async fn make_session(provider: Provider, model: &str, cwd: &Path) -> Session {
|
|||
dotenvy::dotenv().ok();
|
||||
let client = Client::from_env().await.expect("Client::from_env failed");
|
||||
let mut profile = build_profile(provider, model, &client);
|
||||
let env = Arc::new(LocalExecutionEnvironment::new(cwd.to_path_buf()));
|
||||
let env = Arc::new(LocalSandbox::new(cwd.to_path_buf()));
|
||||
|
||||
// Register subagent tools so spawn_agent / wait / send_input / close_agent are available
|
||||
let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(3)));
|
||||
|
|
@ -72,7 +72,7 @@ async fn make_session(provider: Provider, model: &str, cwd: &Path) -> Session {
|
|||
}
|
||||
}
|
||||
};
|
||||
let sub_env = Arc::new(LocalExecutionEnvironment::new(factory_cwd.clone()));
|
||||
let sub_env = Arc::new(LocalSandbox::new(factory_cwd.clone()));
|
||||
Session::new(
|
||||
factory_client.clone(),
|
||||
sub_profile,
|
||||
|
|
@ -99,7 +99,7 @@ async fn make_session_with_config(
|
|||
dotenvy::dotenv().ok();
|
||||
let client = Client::from_env().await.expect("Client::from_env failed");
|
||||
let profile: Arc<dyn ProviderProfile> = Arc::from(build_profile(provider, model, &client));
|
||||
let env = Arc::new(LocalExecutionEnvironment::new(cwd.to_path_buf()));
|
||||
let env = Arc::new(LocalSandbox::new(cwd.to_path_buf()));
|
||||
Session::new(client, profile, env, config)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use clap::Args;
|
|||
|
||||
use crate::server::{build_router, create_app_state_with_options};
|
||||
use arc_workflows::cli::backend::AgentApiBackend;
|
||||
use arc_workflows::cli::ExecutionEnvKind;
|
||||
use arc_workflows::cli::SandboxProvider;
|
||||
use arc_workflows::handler::default_registry;
|
||||
use arc_workflows::interviewer::Interviewer;
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ pub struct ServeArgs {
|
|||
|
||||
/// Execution environment for agent tools
|
||||
#[arg(long, value_enum)]
|
||||
pub execution_env: Option<ExecutionEnvKind>,
|
||||
pub sandbox: Option<SandboxProvider>,
|
||||
|
||||
/// Serve static demo data (disables auth, read-only)
|
||||
#[arg(long)]
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use tokio_stream::StreamExt;
|
|||
|
||||
use tracing::{error, info};
|
||||
|
||||
use arc_agent::LocalExecutionEnvironment;
|
||||
use arc_agent::LocalSandbox;
|
||||
|
||||
use crate::jwt_auth::{AuthMode, AuthenticatedService};
|
||||
use arc_workflows::checkpoint::Checkpoint;
|
||||
|
|
@ -221,13 +221,13 @@ async fn start_run(
|
|||
|
||||
let registry = (state.registry_factory)(Arc::clone(&interviewer) as Arc<dyn Interviewer>);
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
let execution_env: Arc<dyn arc_agent::ExecutionEnvironment> =
|
||||
Arc::new(LocalExecutionEnvironment::new(cwd));
|
||||
let sandbox: Arc<dyn arc_agent::Sandbox> =
|
||||
Arc::new(LocalSandbox::new(cwd));
|
||||
let engine = WorkflowRunEngine::with_interviewer(
|
||||
registry,
|
||||
Arc::new(emitter),
|
||||
Arc::clone(&interviewer) as Arc<dyn Interviewer>,
|
||||
execution_env,
|
||||
sandbox,
|
||||
);
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use chrono::{DateTime, Utc};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use arc_agent::ExecutionEnvironment;
|
||||
use arc_agent::Sandbox;
|
||||
|
||||
use crate::error::{ArcError, Result};
|
||||
|
||||
|
|
@ -251,7 +251,7 @@ pub fn format_artifact_reference(path: &str) -> String {
|
|||
format!("See: {path}")
|
||||
}
|
||||
|
||||
/// Sync artifact files to a remote execution environment.
|
||||
/// Sync artifact files to a remote sandbox.
|
||||
///
|
||||
/// For each `file://` pointer in `updates`, checks whether the file is accessible
|
||||
/// in `env`. If not, reads the local file and uploads it via `env.write_file`,
|
||||
|
|
@ -263,7 +263,7 @@ pub fn format_artifact_reference(path: &str) -> String {
|
|||
/// Returns an error if reading a local artifact or writing to the remote env fails.
|
||||
pub async fn sync_artifacts_to_env(
|
||||
updates: &mut HashMap<String, Value>,
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env: &dyn Sandbox,
|
||||
) -> Result<()> {
|
||||
for value in updates.values_mut() {
|
||||
let local_path = match artifact_path(value) {
|
||||
|
|
@ -512,7 +512,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ExecutionEnvironment for TestSyncEnv {
|
||||
impl Sandbox for TestSyncEnv {
|
||||
async fn read_file(
|
||||
&self,
|
||||
_path: &str,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use async_trait::async_trait;
|
|||
|
||||
use arc_agent::{
|
||||
subagent::{SessionFactory, SubAgentManager},
|
||||
AgentEvent, AnthropicProfile, ExecutionEnvironment, GeminiProfile, OpenAiProfile,
|
||||
AgentEvent, AnthropicProfile, Sandbox, GeminiProfile, OpenAiProfile,
|
||||
ProviderProfile, Session, SessionConfig, Turn,
|
||||
};
|
||||
use arc_llm::client::Client;
|
||||
|
|
@ -45,7 +45,7 @@ impl AgentApiBackend {
|
|||
async fn create_session(
|
||||
&self,
|
||||
node: &Node,
|
||||
execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<Session, ArcError> {
|
||||
let client = Client::from_env()
|
||||
.await
|
||||
|
|
@ -68,7 +68,7 @@ impl AgentApiBackend {
|
|||
let factory_client = client.clone();
|
||||
let factory_provider = self.provider;
|
||||
let factory_model = self.model.clone();
|
||||
let factory_env = Arc::clone(execution_env);
|
||||
let factory_env = Arc::clone(sandbox);
|
||||
let factory: SessionFactory = Arc::new(move || {
|
||||
let child_profile: Arc<dyn ProviderProfile> = match factory_provider {
|
||||
Provider::OpenAi => Arc::new(OpenAiProfile::new(&factory_model)),
|
||||
|
|
@ -89,7 +89,7 @@ impl AgentApiBackend {
|
|||
profile.register_subagent_tools(manager, factory, 0);
|
||||
let profile: Arc<dyn ProviderProfile> = Arc::from(profile);
|
||||
|
||||
let session = Session::new(client, profile, Arc::clone(execution_env), config);
|
||||
let session = Session::new(client, profile, Arc::clone(sandbox), config);
|
||||
|
||||
// Wire subagent event callback to parent session's emitter
|
||||
manager_for_callback
|
||||
|
|
@ -196,7 +196,7 @@ impl CodergenBackend for AgentApiBackend {
|
|||
thread_id: Option<&str>,
|
||||
emitter: &Arc<crate::event::EventEmitter>,
|
||||
stage_dir: &std::path::Path,
|
||||
execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
let fidelity = context.get_string("internal.fidelity", "");
|
||||
let reuse_key = if fidelity == "full" {
|
||||
|
|
@ -211,10 +211,10 @@ impl CodergenBackend for AgentApiBackend {
|
|||
if let Some(s) = existing {
|
||||
(s, true)
|
||||
} else {
|
||||
(self.create_session(node, execution_env).await?, false)
|
||||
(self.create_session(node, sandbox).await?, false)
|
||||
}
|
||||
} else {
|
||||
(self.create_session(node, execution_env).await?, false)
|
||||
(self.create_session(node, sandbox).await?, false)
|
||||
};
|
||||
|
||||
// File change tracking: shared between spawned task and main fn.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_agent::ExecutionEnvironment;
|
||||
use arc_agent::Sandbox;
|
||||
use arc_llm::provider::Provider;
|
||||
use async_trait::async_trait;
|
||||
|
||||
|
|
@ -229,15 +229,15 @@ impl AgentCliBackend {
|
|||
/// Detect changed files by comparing git state before and after the CLI run.
|
||||
async fn detect_changed_files(
|
||||
&self,
|
||||
execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Vec<String> {
|
||||
// Get unstaged changes
|
||||
let diff_result = execution_env
|
||||
let diff_result = sandbox
|
||||
.exec_command("git diff --name-only", 30_000, None, None, None)
|
||||
.await;
|
||||
|
||||
// Get untracked files
|
||||
let untracked_result = execution_env
|
||||
let untracked_result = sandbox
|
||||
.exec_command(
|
||||
"git ls-files --others --exclude-standard",
|
||||
30_000,
|
||||
|
|
@ -289,14 +289,14 @@ impl CodergenBackend for AgentCliBackend {
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
stage_dir: &Path,
|
||||
execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
// 1. Snapshot git state before the CLI run
|
||||
let files_before = self.detect_changed_files(execution_env).await;
|
||||
let files_before = self.detect_changed_files(sandbox).await;
|
||||
|
||||
// 2. Write prompt to temp file
|
||||
let prompt_path = "/tmp/arc_cli_prompt.txt";
|
||||
execution_env
|
||||
sandbox
|
||||
.write_file(prompt_path, prompt)
|
||||
.await
|
||||
.map_err(|e| ArcError::Handler(format!("Failed to write prompt file: {e}")))?;
|
||||
|
|
@ -320,7 +320,7 @@ impl CodergenBackend for AgentCliBackend {
|
|||
let _ = tokio::fs::write(stage_dir.join("provider_used.json"), json).await;
|
||||
}
|
||||
|
||||
let result = execution_env
|
||||
let result = sandbox
|
||||
.exec_command(&command, 600_000, None, None, None)
|
||||
.await
|
||||
.map_err(|e| ArcError::Handler(format!("CLI command failed: {e}")))?;
|
||||
|
|
@ -347,7 +347,7 @@ impl CodergenBackend for AgentCliBackend {
|
|||
.ok_or_else(|| ArcError::Handler("Failed to parse CLI output".to_string()))?;
|
||||
|
||||
// 5. Detect changed files
|
||||
let files_after = self.detect_changed_files(execution_env).await;
|
||||
let files_after = self.detect_changed_files(sandbox).await;
|
||||
let files_touched: Vec<String> = files_after
|
||||
.into_iter()
|
||||
.filter(|f| !files_before.contains(f))
|
||||
|
|
@ -415,7 +415,7 @@ impl CodergenBackend for BackendRouter {
|
|||
thread_id: Option<&str>,
|
||||
emitter: &Arc<EventEmitter>,
|
||||
stage_dir: &Path,
|
||||
execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
if self.should_use_cli(node) {
|
||||
self.cli_backend
|
||||
|
|
@ -426,7 +426,7 @@ impl CodergenBackend for BackendRouter {
|
|||
thread_id,
|
||||
emitter,
|
||||
stage_dir,
|
||||
execution_env,
|
||||
sandbox,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
|
|
@ -438,7 +438,7 @@ impl CodergenBackend for BackendRouter {
|
|||
thread_id,
|
||||
emitter,
|
||||
stage_dir,
|
||||
execution_env,
|
||||
sandbox,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -655,7 +655,7 @@ mod tests {
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
Ok(CodergenResult::Text {
|
||||
text: "stub".to_string(),
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ use crate::outcome::StageUsage;
|
|||
use crate::validation::{Diagnostic, Severity};
|
||||
use arc_agent::AgentEvent;
|
||||
|
||||
/// Execution environment for agent tool operations.
|
||||
/// Sandbox provider for agent tool operations.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
|
||||
pub enum ExecutionEnvKind {
|
||||
pub enum SandboxProvider {
|
||||
/// Run tools on the local host (default)
|
||||
#[default]
|
||||
Local,
|
||||
|
|
@ -30,7 +30,7 @@ pub enum ExecutionEnvKind {
|
|||
Daytona,
|
||||
}
|
||||
|
||||
impl fmt::Display for ExecutionEnvKind {
|
||||
impl fmt::Display for SandboxProvider {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Local => write!(f, "local"),
|
||||
|
|
@ -40,7 +40,7 @@ impl fmt::Display for ExecutionEnvKind {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromStr for ExecutionEnvKind {
|
||||
impl FromStr for SandboxProvider {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
|
|
@ -48,7 +48,7 @@ impl FromStr for ExecutionEnvKind {
|
|||
"local" => Ok(Self::Local),
|
||||
"docker" => Ok(Self::Docker),
|
||||
"daytona" => Ok(Self::Daytona),
|
||||
other => Err(format!("unknown execution environment: {other}")),
|
||||
other => Err(format!("unknown sandbox provider: {other}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ pub struct RunArgs {
|
|||
|
||||
/// Execution environment for agent tools
|
||||
#[arg(long, value_enum)]
|
||||
pub execution_env: Option<ExecutionEnvKind>,
|
||||
pub sandbox: Option<SandboxProvider>,
|
||||
|
||||
/// Attach a label to this run (repeatable, format: KEY=VALUE)
|
||||
#[arg(long = "label", value_name = "KEY=VALUE")]
|
||||
|
|
@ -511,27 +511,27 @@ pub fn format_event_summary(event: &WorkflowRunEvent, styles: &Styles) -> String
|
|||
} => {
|
||||
format!("[SUBGRAPH_COMPLETED] node={node_id} steps={steps_executed} status={status} duration={duration_ms}ms")
|
||||
}
|
||||
WorkflowRunEvent::ExecutionEnv { event } => {
|
||||
use arc_agent::ExecutionEnvEvent;
|
||||
WorkflowRunEvent::Sandbox { event } => {
|
||||
use arc_agent::SandboxEvent;
|
||||
match event {
|
||||
ExecutionEnvEvent::Initializing { env_type } => format!("[EXEC_ENV_INITIALIZING] env_type={env_type}"),
|
||||
ExecutionEnvEvent::Ready { env_type, duration_ms } => format!("[EXEC_ENV_READY] env_type={env_type} duration={duration_ms}ms"),
|
||||
ExecutionEnvEvent::InitializeFailed { env_type, error, duration_ms } => format!("[EXEC_ENV_INIT_FAILED] env_type={env_type} error=\"{error}\" duration={duration_ms}ms"),
|
||||
ExecutionEnvEvent::CleanupStarted { env_type } => format!("[EXEC_ENV_CLEANUP_STARTED] env_type={env_type}"),
|
||||
ExecutionEnvEvent::CleanupCompleted { env_type, duration_ms } => format!("[EXEC_ENV_CLEANUP_COMPLETED] env_type={env_type} duration={duration_ms}ms"),
|
||||
ExecutionEnvEvent::CleanupFailed { env_type, error } => format!("[EXEC_ENV_CLEANUP_FAILED] env_type={env_type} error=\"{error}\""),
|
||||
ExecutionEnvEvent::ImagePulling { image } => format!("[EXEC_ENV_IMAGE_PULLING] image={image}"),
|
||||
ExecutionEnvEvent::ImagePulled { image, duration_ms } => format!("[EXEC_ENV_IMAGE_PULLED] image={image} duration={duration_ms}ms"),
|
||||
ExecutionEnvEvent::SnapshotEnsuring { name } => format!("[EXEC_ENV_SNAPSHOT_ENSURING] name={name}"),
|
||||
ExecutionEnvEvent::SnapshotCreating { name } => format!("[EXEC_ENV_SNAPSHOT_CREATING] name={name}"),
|
||||
ExecutionEnvEvent::SnapshotReady { name, duration_ms } => format!("[EXEC_ENV_SNAPSHOT_READY] name={name} duration={duration_ms}ms"),
|
||||
ExecutionEnvEvent::SnapshotFailed { name, error } => format!("[EXEC_ENV_SNAPSHOT_FAILED] name={name} error=\"{error}\""),
|
||||
ExecutionEnvEvent::GitCloneStarted { url, branch } => {
|
||||
SandboxEvent::Initializing { provider } => format!("[SANDBOX_INITIALIZING] provider={provider}"),
|
||||
SandboxEvent::Ready { provider, duration_ms } => format!("[SANDBOX_READY] provider={provider} duration={duration_ms}ms"),
|
||||
SandboxEvent::InitializeFailed { provider, error, duration_ms } => format!("[SANDBOX_INIT_FAILED] provider={provider} error=\"{error}\" duration={duration_ms}ms"),
|
||||
SandboxEvent::CleanupStarted { provider } => format!("[SANDBOX_CLEANUP_STARTED] provider={provider}"),
|
||||
SandboxEvent::CleanupCompleted { provider, duration_ms } => format!("[SANDBOX_CLEANUP_COMPLETED] provider={provider} duration={duration_ms}ms"),
|
||||
SandboxEvent::CleanupFailed { provider, error } => format!("[SANDBOX_CLEANUP_FAILED] provider={provider} error=\"{error}\""),
|
||||
SandboxEvent::SnapshotPulling { name } => format!("[SANDBOX_SNAPSHOT_PULLING] name={name}"),
|
||||
SandboxEvent::SnapshotPulled { name, duration_ms } => format!("[SANDBOX_SNAPSHOT_PULLED] name={name} duration={duration_ms}ms"),
|
||||
SandboxEvent::SnapshotEnsuring { name } => format!("[SANDBOX_SNAPSHOT_ENSURING] name={name}"),
|
||||
SandboxEvent::SnapshotCreating { name } => format!("[SANDBOX_SNAPSHOT_CREATING] name={name}"),
|
||||
SandboxEvent::SnapshotReady { name, duration_ms } => format!("[SANDBOX_SNAPSHOT_READY] name={name} duration={duration_ms}ms"),
|
||||
SandboxEvent::SnapshotFailed { name, error } => format!("[SANDBOX_SNAPSHOT_FAILED] name={name} error=\"{error}\""),
|
||||
SandboxEvent::GitCloneStarted { url, branch } => {
|
||||
let branch_str = branch.as_deref().unwrap_or("(default)");
|
||||
format!("[EXEC_ENV_GIT_CLONE_STARTED] url={url} branch={branch_str}")
|
||||
format!("[SANDBOX_GIT_CLONE_STARTED] url={url} branch={branch_str}")
|
||||
}
|
||||
ExecutionEnvEvent::GitCloneCompleted { url, duration_ms } => format!("[EXEC_ENV_GIT_CLONE_COMPLETED] url={url} duration={duration_ms}ms"),
|
||||
ExecutionEnvEvent::GitCloneFailed { url, error } => format!("[EXEC_ENV_GIT_CLONE_FAILED] url={url} error=\"{error}\""),
|
||||
SandboxEvent::GitCloneCompleted { url, duration_ms } => format!("[SANDBOX_GIT_CLONE_COMPLETED] url={url} duration={duration_ms}ms"),
|
||||
SandboxEvent::GitCloneFailed { url, error } => format!("[SANDBOX_GIT_CLONE_FAILED] url={url} error=\"{error}\""),
|
||||
}
|
||||
}
|
||||
WorkflowRunEvent::SetupStarted { command_count } => {
|
||||
|
|
@ -846,54 +846,54 @@ pub fn format_event_detail(event: &WorkflowRunEvent, styles: &Styles) -> String
|
|||
} => {
|
||||
format!("{d}── SUBGRAPH_COMPLETED ───────────────────────{r}\n {d}node_id:{r} {node_id}\n {d}steps_executed:{r} {steps_executed}\n {d}status:{r} {status}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
}
|
||||
WorkflowRunEvent::ExecutionEnv { event } => {
|
||||
use arc_agent::ExecutionEnvEvent;
|
||||
WorkflowRunEvent::Sandbox { event } => {
|
||||
use arc_agent::SandboxEvent;
|
||||
match event {
|
||||
ExecutionEnvEvent::Initializing { env_type } => {
|
||||
format!("{d}── EXEC_ENV_INITIALIZING ────────────────────{r}\n {d}env_type:{r} {env_type}\n")
|
||||
SandboxEvent::Initializing { provider } => {
|
||||
format!("{d}── SANDBOX_INITIALIZING ────────────────────{r}\n {d}provider:{r} {provider}\n")
|
||||
}
|
||||
ExecutionEnvEvent::Ready { env_type, duration_ms } => {
|
||||
format!("{d}── EXEC_ENV_READY ───────────────────────────{r}\n {d}env_type:{r} {env_type}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
SandboxEvent::Ready { provider, duration_ms } => {
|
||||
format!("{d}── SANDBOX_READY ───────────────────────────{r}\n {d}provider:{r} {provider}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
}
|
||||
ExecutionEnvEvent::InitializeFailed { env_type, error, duration_ms } => {
|
||||
format!("{d}── EXEC_ENV_INIT_FAILED ─────────────────────{r}\n {d}env_type:{r} {env_type}\n {d}error:{r} {error}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
SandboxEvent::InitializeFailed { provider, error, duration_ms } => {
|
||||
format!("{d}── SANDBOX_INIT_FAILED ─────────────────────{r}\n {d}provider:{r} {provider}\n {d}error:{r} {error}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
}
|
||||
ExecutionEnvEvent::CleanupStarted { env_type } => {
|
||||
format!("{d}── EXEC_ENV_CLEANUP_STARTED ─────────────────{r}\n {d}env_type:{r} {env_type}\n")
|
||||
SandboxEvent::CleanupStarted { provider } => {
|
||||
format!("{d}── SANDBOX_CLEANUP_STARTED ─────────────────{r}\n {d}provider:{r} {provider}\n")
|
||||
}
|
||||
ExecutionEnvEvent::CleanupCompleted { env_type, duration_ms } => {
|
||||
format!("{d}── EXEC_ENV_CLEANUP_COMPLETED ───────────────{r}\n {d}env_type:{r} {env_type}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
SandboxEvent::CleanupCompleted { provider, duration_ms } => {
|
||||
format!("{d}── SANDBOX_CLEANUP_COMPLETED ───────────────{r}\n {d}provider:{r} {provider}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
}
|
||||
ExecutionEnvEvent::CleanupFailed { env_type, error } => {
|
||||
format!("{d}── EXEC_ENV_CLEANUP_FAILED ──────────────────{r}\n {d}env_type:{r} {env_type}\n {d}error:{r} {error}\n")
|
||||
SandboxEvent::CleanupFailed { provider, error } => {
|
||||
format!("{d}── SANDBOX_CLEANUP_FAILED ──────────────────{r}\n {d}provider:{r} {provider}\n {d}error:{r} {error}\n")
|
||||
}
|
||||
ExecutionEnvEvent::ImagePulling { image } => {
|
||||
format!("{d}── EXEC_ENV_IMAGE_PULLING ───────────────────{r}\n {d}image:{r} {image}\n")
|
||||
SandboxEvent::SnapshotPulling { name } => {
|
||||
format!("{d}── SANDBOX_SNAPSHOT_PULLING ───────────────────{r}\n {d}image:{r} {name}\n")
|
||||
}
|
||||
ExecutionEnvEvent::ImagePulled { image, duration_ms } => {
|
||||
format!("{d}── EXEC_ENV_IMAGE_PULLED ────────────────────{r}\n {d}image:{r} {image}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
SandboxEvent::SnapshotPulled { name, duration_ms } => {
|
||||
format!("{d}── SANDBOX_SNAPSHOT_PULLED ────────────────────{r}\n {d}image:{r} {name}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
}
|
||||
ExecutionEnvEvent::SnapshotEnsuring { name } => {
|
||||
format!("{d}── EXEC_ENV_SNAPSHOT_ENSURING ───────────────{r}\n {d}name:{r} {name}\n")
|
||||
SandboxEvent::SnapshotEnsuring { name } => {
|
||||
format!("{d}── SANDBOX_SNAPSHOT_ENSURING ───────────────{r}\n {d}name:{r} {name}\n")
|
||||
}
|
||||
ExecutionEnvEvent::SnapshotCreating { name } => {
|
||||
format!("{d}── EXEC_ENV_SNAPSHOT_CREATING ───────────────{r}\n {d}name:{r} {name}\n")
|
||||
SandboxEvent::SnapshotCreating { name } => {
|
||||
format!("{d}── SANDBOX_SNAPSHOT_CREATING ───────────────{r}\n {d}name:{r} {name}\n")
|
||||
}
|
||||
ExecutionEnvEvent::SnapshotReady { name, duration_ms } => {
|
||||
format!("{d}── EXEC_ENV_SNAPSHOT_READY ──────────────────{r}\n {d}name:{r} {name}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
SandboxEvent::SnapshotReady { name, duration_ms } => {
|
||||
format!("{d}── SANDBOX_SNAPSHOT_READY ──────────────────{r}\n {d}name:{r} {name}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
}
|
||||
ExecutionEnvEvent::SnapshotFailed { name, error } => {
|
||||
format!("{d}── EXEC_ENV_SNAPSHOT_FAILED ─────────────────{r}\n {d}name:{r} {name}\n {d}error:{r} {error}\n")
|
||||
SandboxEvent::SnapshotFailed { name, error } => {
|
||||
format!("{d}── SANDBOX_SNAPSHOT_FAILED ─────────────────{r}\n {d}name:{r} {name}\n {d}error:{r} {error}\n")
|
||||
}
|
||||
ExecutionEnvEvent::GitCloneStarted { url, branch } => {
|
||||
SandboxEvent::GitCloneStarted { url, branch } => {
|
||||
let branch_str = branch.as_deref().unwrap_or("(default)");
|
||||
format!("{d}── EXEC_ENV_GIT_CLONE_STARTED ──────────────{r}\n {d}url:{r} {url}\n {d}branch:{r} {branch_str}\n")
|
||||
format!("{d}── SANDBOX_GIT_CLONE_STARTED ──────────────{r}\n {d}url:{r} {url}\n {d}branch:{r} {branch_str}\n")
|
||||
}
|
||||
ExecutionEnvEvent::GitCloneCompleted { url, duration_ms } => {
|
||||
format!("{d}── EXEC_ENV_GIT_CLONE_COMPLETED ────────────{r}\n {d}url:{r} {url}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
SandboxEvent::GitCloneCompleted { url, duration_ms } => {
|
||||
format!("{d}── SANDBOX_GIT_CLONE_COMPLETED ────────────{r}\n {d}url:{r} {url}\n {d}duration_ms:{r} {duration_ms}\n")
|
||||
}
|
||||
ExecutionEnvEvent::GitCloneFailed { url, error } => {
|
||||
format!("{d}── EXEC_ENV_GIT_CLONE_FAILED ───────────────{r}\n {d}url:{r} {url}\n {d}error:{r} {error}\n")
|
||||
SandboxEvent::GitCloneFailed { url, error } => {
|
||||
format!("{d}── SANDBOX_GIT_CLONE_FAILED ───────────────{r}\n {d}url:{r} {url}\n {d}error:{r} {error}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -951,36 +951,36 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn execution_env_kind_default_is_local() {
|
||||
assert_eq!(ExecutionEnvKind::default(), ExecutionEnvKind::Local);
|
||||
fn sandbox_provider_default_is_local() {
|
||||
assert_eq!(SandboxProvider::default(), SandboxProvider::Local);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_env_kind_from_str() {
|
||||
fn sandbox_provider_from_str() {
|
||||
assert_eq!(
|
||||
"local".parse::<ExecutionEnvKind>().unwrap(),
|
||||
ExecutionEnvKind::Local
|
||||
"local".parse::<SandboxProvider>().unwrap(),
|
||||
SandboxProvider::Local
|
||||
);
|
||||
assert_eq!(
|
||||
"docker".parse::<ExecutionEnvKind>().unwrap(),
|
||||
ExecutionEnvKind::Docker
|
||||
"docker".parse::<SandboxProvider>().unwrap(),
|
||||
SandboxProvider::Docker
|
||||
);
|
||||
assert_eq!(
|
||||
"daytona".parse::<ExecutionEnvKind>().unwrap(),
|
||||
ExecutionEnvKind::Daytona
|
||||
"daytona".parse::<SandboxProvider>().unwrap(),
|
||||
SandboxProvider::Daytona
|
||||
);
|
||||
assert_eq!(
|
||||
"LOCAL".parse::<ExecutionEnvKind>().unwrap(),
|
||||
ExecutionEnvKind::Local
|
||||
"LOCAL".parse::<SandboxProvider>().unwrap(),
|
||||
SandboxProvider::Local
|
||||
);
|
||||
assert!("invalid".parse::<ExecutionEnvKind>().is_err());
|
||||
assert!("invalid".parse::<SandboxProvider>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_env_kind_display() {
|
||||
assert_eq!(ExecutionEnvKind::Local.to_string(), "local");
|
||||
assert_eq!(ExecutionEnvKind::Docker.to_string(), "docker");
|
||||
assert_eq!(ExecutionEnvKind::Daytona.to_string(), "daytona");
|
||||
fn sandbox_provider_display() {
|
||||
assert_eq!(SandboxProvider::Local.to_string(), "local");
|
||||
assert_eq!(SandboxProvider::Docker.to_string(), "docker");
|
||||
assert_eq!(SandboxProvider::Daytona.to_string(), "daytona");
|
||||
}
|
||||
|
||||
fn test_styles() -> &'static Styles {
|
||||
|
|
@ -988,14 +988,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn format_summary_execution_env_initializing() {
|
||||
let event = WorkflowRunEvent::ExecutionEnv {
|
||||
event: arc_agent::ExecutionEnvEvent::Initializing {
|
||||
env_type: "docker".into(),
|
||||
fn format_summary_sandbox_initializing() {
|
||||
let event = WorkflowRunEvent::Sandbox {
|
||||
event: arc_agent::SandboxEvent::Initializing {
|
||||
provider: "docker".into(),
|
||||
},
|
||||
};
|
||||
let s = format_event_summary(&event, test_styles());
|
||||
assert!(s.contains("[EXEC_ENV_INITIALIZING]"));
|
||||
assert!(s.contains("[SANDBOX_INITIALIZING]"));
|
||||
assert!(s.contains("docker"));
|
||||
}
|
||||
|
||||
|
|
@ -1008,15 +1008,15 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn format_detail_execution_env_ready() {
|
||||
let event = WorkflowRunEvent::ExecutionEnv {
|
||||
event: arc_agent::ExecutionEnvEvent::Ready {
|
||||
env_type: "local".into(),
|
||||
fn format_detail_sandbox_ready() {
|
||||
let event = WorkflowRunEvent::Sandbox {
|
||||
event: arc_agent::SandboxEvent::Ready {
|
||||
provider: "local".into(),
|
||||
duration_ms: 42,
|
||||
},
|
||||
};
|
||||
let s = format_event_detail(&event, test_styles());
|
||||
assert!(s.contains("EXEC_ENV_READY"));
|
||||
assert!(s.contains("SANDBOX_READY"));
|
||||
assert!(s.contains("local"));
|
||||
assert!(s.contains("42"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::time::Instant;
|
|||
|
||||
use anyhow::bail;
|
||||
use arc_agent::{
|
||||
DockerConfig, DockerExecutionEnvironment, ExecutionEnvironment, LocalExecutionEnvironment,
|
||||
DockerSandboxConfig, DockerSandbox, Sandbox, LocalSandbox,
|
||||
};
|
||||
use arc_util::terminal::Styles;
|
||||
use chrono::{Local, Utc};
|
||||
|
|
@ -28,7 +28,7 @@ use super::cli_backend::{BackendRouter, AgentCliBackend};
|
|||
use super::task_config;
|
||||
use super::{
|
||||
compute_stage_cost, format_cost, format_duration_human, format_event_detail,
|
||||
format_event_summary, format_tokens_human, print_diagnostics, read_dot_file, ExecutionEnvKind,
|
||||
format_event_summary, format_tokens_human, print_diagnostics, read_dot_file, SandboxProvider,
|
||||
RunArgs,
|
||||
};
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
}
|
||||
}
|
||||
|
||||
// Collect setup commands — they'll be run inside the execution environment
|
||||
// Collect setup commands — they'll be run inside the sandbox
|
||||
let setup_commands: Vec<String> = task_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.setup.as_ref())
|
||||
|
|
@ -118,23 +118,23 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
|
||||
// 2. Pre-flight: check git cleanliness before creating any files
|
||||
// (must happen before logs dir is created, which may be inside the repo)
|
||||
let execution_env_kind_preview = {
|
||||
let sandbox_provider_preview = {
|
||||
let toml_exec = task_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.execution.as_ref())
|
||||
.and_then(|e| e.environment.as_deref())
|
||||
.map(|s| s.parse::<ExecutionEnvKind>())
|
||||
.and_then(|c| c.sandbox.as_ref())
|
||||
.and_then(|e| e.provider.as_deref())
|
||||
.map(|s| s.parse::<SandboxProvider>())
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten();
|
||||
args.execution_env.or(toml_exec).unwrap_or_default()
|
||||
args.sandbox.or(toml_exec).unwrap_or_default()
|
||||
};
|
||||
let original_cwd = std::env::current_dir()?;
|
||||
let git_clean = match execution_env_kind_preview {
|
||||
ExecutionEnvKind::Local | ExecutionEnvKind::Docker => {
|
||||
let git_clean = match sandbox_provider_preview {
|
||||
SandboxProvider::Local | SandboxProvider::Docker => {
|
||||
crate::git::ensure_clean(&original_cwd).is_ok()
|
||||
}
|
||||
ExecutionEnvKind::Daytona => false,
|
||||
SandboxProvider::Daytona => false,
|
||||
};
|
||||
|
||||
// 3. Create logs directory
|
||||
|
|
@ -284,17 +284,17 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
Arc::new(ConsoleInterviewer::new(styles))
|
||||
};
|
||||
|
||||
// 5. Resolve execution environment: CLI flag > TOML > default
|
||||
let toml_execution_env = task_cfg
|
||||
// 5. Resolve sandbox: CLI flag > TOML > default
|
||||
let toml_sandbox = task_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.execution.as_ref())
|
||||
.and_then(|e| e.environment.as_deref())
|
||||
.map(|s| s.parse::<ExecutionEnvKind>())
|
||||
.and_then(|c| c.sandbox.as_ref())
|
||||
.and_then(|e| e.provider.as_deref())
|
||||
.map(|s| s.parse::<SandboxProvider>())
|
||||
.transpose()
|
||||
.map_err(|e| anyhow::anyhow!("Invalid execution environment in TOML: {e}"))?;
|
||||
let execution_env_kind = args
|
||||
.execution_env
|
||||
.or(toml_execution_env)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid sandbox in TOML: {e}"))?;
|
||||
let sandbox_provider = args
|
||||
.sandbox
|
||||
.or(toml_sandbox)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Set up git worktree for local execution (must happen before cwd is captured)
|
||||
|
|
@ -319,75 +319,75 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let daytona_config = task_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.execution.as_ref())
|
||||
.and_then(|c| c.sandbox.as_ref())
|
||||
.and_then(|e| e.daytona.clone());
|
||||
|
||||
// Wrap emitter in Arc now so we can share it with exec env callbacks
|
||||
let emitter = Arc::new(emitter);
|
||||
|
||||
let execution_env: Arc<dyn ExecutionEnvironment> = match execution_env_kind {
|
||||
ExecutionEnvKind::Docker => {
|
||||
let config = DockerConfig {
|
||||
let sandbox: Arc<dyn Sandbox> = match sandbox_provider {
|
||||
SandboxProvider::Docker => {
|
||||
let config = DockerSandboxConfig {
|
||||
host_working_directory: cwd.to_string_lossy().to_string(),
|
||||
..DockerConfig::default()
|
||||
..DockerSandboxConfig::default()
|
||||
};
|
||||
let mut env = DockerExecutionEnvironment::new(config)
|
||||
let mut env = DockerSandbox::new(config)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create Docker environment: {e}"))?;
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::ExecutionEnv { event });
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
}
|
||||
ExecutionEnvKind::Daytona => {
|
||||
SandboxProvider::Daytona => {
|
||||
let daytona_client = daytona_sdk::Client::new()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create Daytona client: {e}"))?;
|
||||
let config = daytona_config.clone().unwrap_or_default();
|
||||
let mut env =
|
||||
crate::daytona_env::DaytonaExecutionEnvironment::new(daytona_client, config);
|
||||
crate::daytona_sandbox::DaytonaSandbox::new(daytona_client, config);
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::ExecutionEnv { event });
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
}
|
||||
ExecutionEnvKind::Local => {
|
||||
let mut env = LocalExecutionEnvironment::new(cwd);
|
||||
SandboxProvider::Local => {
|
||||
let mut env = LocalSandbox::new(cwd);
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::ExecutionEnv { event });
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize execution environment (creates sandbox/container once for the whole run)
|
||||
execution_env
|
||||
// Initialize sandbox (creates sandbox/container once for the whole run)
|
||||
sandbox
|
||||
.initialize()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to initialize execution environment: {e}"))?;
|
||||
.map_err(|e| anyhow::anyhow!("Failed to initialize sandbox: {e}"))?;
|
||||
|
||||
// Ensure cleanup runs even on error/panic
|
||||
let exec_env_for_cleanup = Arc::clone(&execution_env);
|
||||
let sandbox_for_cleanup = Arc::clone(&sandbox);
|
||||
let _cleanup_guard = scopeguard::guard((), move |()| {
|
||||
// Best-effort cleanup — fire and forget in a blocking context
|
||||
let rt = tokio::runtime::Handle::try_current();
|
||||
if let Ok(handle) = rt {
|
||||
handle.spawn(async move {
|
||||
if let Err(e) = exec_env_for_cleanup.cleanup().await {
|
||||
if let Err(e) = sandbox_for_cleanup.cleanup().await {
|
||||
tracing::warn!(error = %e, "Execution environment cleanup failed");
|
||||
eprintln!("Warning: execution environment cleanup failed: {e}");
|
||||
eprintln!("Warning: sandbox cleanup failed: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Set up git inside Daytona sandbox (if applicable)
|
||||
let (daytona_run_id, daytona_base_sha, daytona_branch) = if execution_env_kind
|
||||
== ExecutionEnvKind::Daytona
|
||||
let (daytona_run_id, daytona_base_sha, daytona_branch) = if sandbox_provider
|
||||
== SandboxProvider::Daytona
|
||||
{
|
||||
match setup_daytona_git(&*execution_env).await {
|
||||
match setup_daytona_git(&*sandbox).await {
|
||||
Ok((rid, base, branch)) => (Some(rid), Some(base), Some(branch)),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
|
|
@ -401,7 +401,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
(None, None, None)
|
||||
};
|
||||
|
||||
// Run setup commands inside the execution environment (once, not per-stage)
|
||||
// Run setup commands inside the sandbox (once, not per-stage)
|
||||
if !setup_commands.is_empty() {
|
||||
emitter.emit(&crate::event::WorkflowRunEvent::SetupStarted {
|
||||
command_count: setup_commands.len(),
|
||||
|
|
@ -413,7 +413,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
index,
|
||||
});
|
||||
let cmd_start = Instant::now();
|
||||
let result = execution_env
|
||||
let result = sandbox
|
||||
.exec_command(cmd, 300_000, None, None, None)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Setup command failed: {e}"))?;
|
||||
|
|
@ -533,7 +533,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
registry,
|
||||
Arc::clone(&emitter),
|
||||
interviewer,
|
||||
Arc::clone(&execution_env),
|
||||
Arc::clone(&sandbox),
|
||||
);
|
||||
|
||||
// 7. Execute
|
||||
|
|
@ -551,11 +551,11 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
cancel_token: None,
|
||||
dry_run: dry_run_mode,
|
||||
run_id,
|
||||
git_checkpoint: match execution_env_kind {
|
||||
ExecutionEnvKind::Local | ExecutionEnvKind::Docker => {
|
||||
git_checkpoint: match sandbox_provider {
|
||||
SandboxProvider::Local | SandboxProvider::Docker => {
|
||||
worktree_work_dir.map(GitCheckpointMode::Host)
|
||||
}
|
||||
ExecutionEnvKind::Daytona => daytona_base_sha
|
||||
SandboxProvider::Daytona => daytona_base_sha
|
||||
.as_ref()
|
||||
.map(|_| GitCheckpointMode::Remote(original_cwd.clone())),
|
||||
},
|
||||
|
|
@ -622,7 +622,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
run_duration_ms,
|
||||
dry_run_mode,
|
||||
llm_client.as_ref(),
|
||||
&execution_env,
|
||||
&sandbox,
|
||||
provider_enum,
|
||||
&model,
|
||||
styles,
|
||||
|
|
@ -738,10 +738,10 @@ fn setup_worktree(
|
|||
/// Set up git inside a Daytona sandbox for checkpoint commits.
|
||||
/// Returns (run_id, base_sha, branch_name) on success.
|
||||
async fn setup_daytona_git(
|
||||
exec_env: &dyn arc_agent::ExecutionEnvironment,
|
||||
sandbox: &dyn arc_agent::Sandbox,
|
||||
) -> anyhow::Result<(String, String, String)> {
|
||||
// Get current HEAD as base SHA
|
||||
let sha_result = exec_env
|
||||
let sha_result = sandbox
|
||||
.exec_command("git rev-parse HEAD", 10_000, None, None, None)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("git rev-parse HEAD failed: {e}"))?;
|
||||
|
|
@ -759,7 +759,7 @@ async fn setup_daytona_git(
|
|||
|
||||
// Create and checkout a run branch
|
||||
let checkout_cmd = format!("git checkout -b {branch_name}");
|
||||
let checkout_result = exec_env
|
||||
let checkout_result = sandbox
|
||||
.exec_command(&checkout_cmd, 10_000, None, None, None)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("git checkout failed: {e}"))?;
|
||||
|
|
@ -854,13 +854,13 @@ async fn run_from_branch(
|
|||
let base_sha = crate::git::MetadataStore::read_manifest(&original_cwd, &run_id)?
|
||||
.and_then(|m| m.get("base_sha").and_then(|v| v.as_str()).map(String::from));
|
||||
|
||||
// Build minimal execution environment (local only for now)
|
||||
// Build minimal sandbox (local only for now)
|
||||
let emitter = Arc::new(EventEmitter::new());
|
||||
let execution_env: Arc<dyn arc_agent::ExecutionEnvironment> = {
|
||||
let mut env = arc_agent::LocalExecutionEnvironment::new(worktree_path.clone());
|
||||
let sandbox: Arc<dyn arc_agent::Sandbox> = {
|
||||
let mut env = arc_agent::LocalSandbox::new(worktree_path.clone());
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::ExecutionEnv { event });
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
};
|
||||
|
|
@ -901,7 +901,7 @@ async fn run_from_branch(
|
|||
registry,
|
||||
Arc::clone(&emitter),
|
||||
interviewer,
|
||||
Arc::clone(&execution_env),
|
||||
Arc::clone(&sandbox),
|
||||
);
|
||||
|
||||
let meta_branch = Some(crate::git::MetadataStore::branch_name(&run_id));
|
||||
|
|
@ -950,7 +950,7 @@ async fn run_from_branch(
|
|||
run_duration_ms,
|
||||
dry_run_mode,
|
||||
llm_client.as_ref(),
|
||||
&execution_env,
|
||||
&sandbox,
|
||||
provider_enum,
|
||||
&model,
|
||||
styles,
|
||||
|
|
@ -1006,7 +1006,7 @@ async fn generate_retro(
|
|||
run_duration_ms: u64,
|
||||
dry_run_mode: bool,
|
||||
llm_client: Option<&arc_llm::client::Client>,
|
||||
execution_env: &Arc<dyn arc_agent::ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn arc_agent::Sandbox>,
|
||||
provider_enum: Provider,
|
||||
model: &str,
|
||||
styles: &'static Styles,
|
||||
|
|
@ -1050,7 +1050,7 @@ async fn generate_retro(
|
|||
let narrative_result = if dry_run_mode {
|
||||
Ok(crate::retro_agent::dry_run_narrative())
|
||||
} else if let Some(client) = llm_client {
|
||||
crate::retro_agent::run_retro_agent(execution_env, logs_dir, client, provider_enum, model)
|
||||
crate::retro_agent::run_retro_agent(sandbox, logs_dir, client, provider_enum, model)
|
||||
.await
|
||||
} else {
|
||||
Err(anyhow::anyhow!("No LLM client available"))
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
|||
use anyhow::{bail, Context};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::daytona_env::DaytonaConfig;
|
||||
use crate::daytona_sandbox::DaytonaConfig;
|
||||
|
||||
const SUPPORTED_VERSION: u32 = 1;
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ pub struct TaskConfig {
|
|||
pub directory: Option<String>,
|
||||
pub llm: Option<LlmConfig>,
|
||||
pub setup: Option<SetupConfig>,
|
||||
pub execution: Option<ExecutionConfig>,
|
||||
pub sandbox: Option<SandboxConfig>,
|
||||
pub vars: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
|
|
@ -37,8 +37,8 @@ pub struct SetupConfig {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ExecutionConfig {
|
||||
pub environment: Option<String>,
|
||||
pub struct SandboxConfig {
|
||||
pub provider: Option<String>,
|
||||
pub daytona: Option<DaytonaConfig>,
|
||||
}
|
||||
|
||||
|
|
@ -220,19 +220,19 @@ language = "python"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn parse_toml_with_execution() {
|
||||
fn parse_toml_with_sandbox() {
|
||||
let toml = r#"
|
||||
version = 1
|
||||
task = "Run tests"
|
||||
graph = "workflow.dot"
|
||||
|
||||
[execution]
|
||||
environment = "daytona"
|
||||
[sandbox]
|
||||
provider = "daytona"
|
||||
"#;
|
||||
let config = parse_task_config(toml).unwrap();
|
||||
let execution = config.execution.unwrap();
|
||||
assert_eq!(execution.environment.as_deref(), Some("daytona"));
|
||||
assert!(execution.daytona.is_none());
|
||||
let sandbox = config.sandbox.unwrap();
|
||||
assert_eq!(sandbox.provider.as_deref(), Some("daytona"));
|
||||
assert!(sandbox.daytona.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -242,16 +242,16 @@ version = 1
|
|||
task = "Run tests"
|
||||
graph = "workflow.dot"
|
||||
|
||||
[execution]
|
||||
environment = "daytona"
|
||||
[sandbox]
|
||||
provider = "daytona"
|
||||
|
||||
[execution.daytona.sandbox]
|
||||
[sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
|
||||
[execution.daytona.sandbox.labels]
|
||||
[sandbox.daytona.labels]
|
||||
project = "arc"
|
||||
|
||||
[execution.daytona.snapshot]
|
||||
[sandbox.daytona.snapshot]
|
||||
name = "my-snapshot"
|
||||
cpu = 4
|
||||
memory = 8
|
||||
|
|
@ -259,12 +259,12 @@ disk = 10
|
|||
dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update"
|
||||
"#;
|
||||
let config = parse_task_config(toml).unwrap();
|
||||
let execution = config.execution.unwrap();
|
||||
assert_eq!(execution.environment.as_deref(), Some("daytona"));
|
||||
let sandbox = config.sandbox.unwrap();
|
||||
assert_eq!(sandbox.provider.as_deref(), Some("daytona"));
|
||||
|
||||
let daytona = execution.daytona.unwrap();
|
||||
assert_eq!(daytona.sandbox.auto_stop_interval, Some(60));
|
||||
let labels = daytona.sandbox.labels.unwrap();
|
||||
let daytona = sandbox.daytona.unwrap();
|
||||
assert_eq!(daytona.auto_stop_interval, Some(60));
|
||||
let labels = daytona.labels.unwrap();
|
||||
assert_eq!(labels["project"], "arc");
|
||||
|
||||
let snapshot = daytona.snapshot.unwrap();
|
||||
|
|
@ -285,15 +285,15 @@ version = 1
|
|||
task = "Run tests"
|
||||
graph = "workflow.dot"
|
||||
|
||||
[execution]
|
||||
environment = "daytona"
|
||||
[sandbox]
|
||||
provider = "daytona"
|
||||
|
||||
[execution.daytona.sandbox]
|
||||
[sandbox.daytona]
|
||||
auto_stop_interval = 30
|
||||
"#;
|
||||
let config = parse_task_config(toml).unwrap();
|
||||
let daytona = config.execution.unwrap().daytona.unwrap();
|
||||
assert_eq!(daytona.sandbox.auto_stop_interval, Some(30));
|
||||
let daytona = config.sandbox.unwrap().daytona.unwrap();
|
||||
assert_eq!(daytona.auto_stop_interval, Some(30));
|
||||
assert!(daytona.snapshot.is_none());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use std::collections::HashMap;
|
|||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
use arc_agent::execution_env::{
|
||||
format_lines_numbered, DirEntry, ExecEnvEventCallback, ExecResult, ExecutionEnvEvent,
|
||||
ExecutionEnvironment, GrepOptions,
|
||||
use arc_agent::sandbox::{
|
||||
format_lines_numbered, DirEntry, SandboxEventCallback, ExecResult, SandboxEvent,
|
||||
Sandbox, GrepOptions,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use rand::Rng;
|
||||
|
|
@ -13,23 +13,15 @@ use serde::Deserialize;
|
|||
const WORKING_DIRECTORY: &str = "/home/daytona/workspace";
|
||||
const DEFAULT_IMAGE: &str = "ubuntu:22.04";
|
||||
|
||||
/// Configuration for a Daytona cloud sandbox execution environment.
|
||||
/// Configuration for a Daytona cloud sandbox.
|
||||
///
|
||||
/// Doubles as the TOML deserialization target for `[execution.daytona]`.
|
||||
/// Doubles as the TOML deserialization target for `[sandbox.daytona]`.
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DaytonaConfig {
|
||||
#[serde(default)]
|
||||
pub sandbox: DaytonaSandboxConfig,
|
||||
pub snapshot: Option<DaytonaSnapshotConfig>,
|
||||
}
|
||||
|
||||
/// Sandbox-level settings (labels, auto-stop).
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DaytonaSandboxConfig {
|
||||
pub auto_stop_interval: Option<i32>,
|
||||
pub labels: Option<HashMap<String, String>>,
|
||||
pub snapshot: Option<DaytonaSnapshotConfig>,
|
||||
}
|
||||
|
||||
/// Snapshot configuration: when present, the sandbox is created from a snapshot
|
||||
|
|
@ -44,16 +36,16 @@ pub struct DaytonaSnapshotConfig {
|
|||
pub dockerfile: Option<String>,
|
||||
}
|
||||
|
||||
/// Execution environment that runs all operations inside a Daytona cloud sandbox.
|
||||
pub struct DaytonaExecutionEnvironment {
|
||||
/// Sandbox that runs all operations inside a Daytona cloud sandbox.
|
||||
pub struct DaytonaSandbox {
|
||||
config: DaytonaConfig,
|
||||
client: daytona_sdk::Client,
|
||||
sandbox: tokio::sync::OnceCell<daytona_sdk::Sandbox>,
|
||||
rg_available: tokio::sync::OnceCell<bool>,
|
||||
event_callback: Option<ExecEnvEventCallback>,
|
||||
event_callback: Option<SandboxEventCallback>,
|
||||
}
|
||||
|
||||
impl DaytonaExecutionEnvironment {
|
||||
impl DaytonaSandbox {
|
||||
#[must_use]
|
||||
pub fn new(client: daytona_sdk::Client, config: DaytonaConfig) -> Self {
|
||||
Self {
|
||||
|
|
@ -65,11 +57,11 @@ impl DaytonaExecutionEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_event_callback(&mut self, cb: ExecEnvEventCallback) {
|
||||
pub fn set_event_callback(&mut self, cb: SandboxEventCallback) {
|
||||
self.event_callback = Some(cb);
|
||||
}
|
||||
|
||||
fn emit(&self, event: ExecutionEnvEvent) {
|
||||
fn emit(&self, event: SandboxEvent) {
|
||||
event.trace();
|
||||
if let Some(ref cb) = self.event_callback {
|
||||
cb(event);
|
||||
|
|
@ -101,8 +93,8 @@ impl DaytonaExecutionEnvironment {
|
|||
);
|
||||
daytona_sdk::SandboxBaseParams {
|
||||
name: Some(name),
|
||||
auto_stop_interval: self.config.sandbox.auto_stop_interval,
|
||||
labels: self.config.sandbox.labels.clone(),
|
||||
auto_stop_interval: self.config.auto_stop_interval,
|
||||
labels: self.config.labels.clone(),
|
||||
ephemeral: Some(true),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -270,36 +262,36 @@ pub fn get_gh_token() -> Result<String, String> {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
||||
impl Sandbox for DaytonaSandbox {
|
||||
async fn initialize(&self) -> Result<(), String> {
|
||||
self.emit(ExecutionEnvEvent::Initializing {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::Initializing {
|
||||
provider: "daytona".into(),
|
||||
});
|
||||
let init_start = Instant::now();
|
||||
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
|
||||
let params = if let Some(ref snap_cfg) = self.config.snapshot {
|
||||
self.emit(ExecutionEnvEvent::SnapshotEnsuring {
|
||||
self.emit(SandboxEvent::SnapshotEnsuring {
|
||||
name: snap_cfg.name.clone(),
|
||||
});
|
||||
let snap_start = Instant::now();
|
||||
if let Err(e) = self.ensure_snapshot(snap_cfg).await {
|
||||
self.emit(ExecutionEnvEvent::SnapshotFailed {
|
||||
self.emit(SandboxEvent::SnapshotFailed {
|
||||
name: snap_cfg.name.clone(),
|
||||
error: e.clone(),
|
||||
});
|
||||
let duration_ms =
|
||||
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::InitializeFailed {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::InitializeFailed {
|
||||
provider: "daytona".into(),
|
||||
error: e.clone(),
|
||||
duration_ms,
|
||||
});
|
||||
return Err(e);
|
||||
}
|
||||
let snap_duration = u64::try_from(snap_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::SnapshotReady {
|
||||
self.emit(SandboxEvent::SnapshotReady {
|
||||
name: snap_cfg.name.clone(),
|
||||
duration_ms: snap_duration,
|
||||
});
|
||||
|
|
@ -325,8 +317,8 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
|||
let err = format!("Failed to create Daytona sandbox: {e}");
|
||||
let duration_ms =
|
||||
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::InitializeFailed {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::InitializeFailed {
|
||||
provider: "daytona".into(),
|
||||
error: err.clone(),
|
||||
duration_ms,
|
||||
});
|
||||
|
|
@ -338,7 +330,7 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
|||
Ok((detected_url, branch)) => {
|
||||
// Daytona clones over HTTPS with token auth, so rewrite SSH URLs.
|
||||
let url = ssh_url_to_https(&detected_url);
|
||||
self.emit(ExecutionEnvEvent::GitCloneStarted {
|
||||
self.emit(SandboxEvent::GitCloneStarted {
|
||||
url: url.clone(),
|
||||
branch: branch.clone(),
|
||||
});
|
||||
|
|
@ -349,14 +341,14 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
|||
let token = match token {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.emit(ExecutionEnvEvent::GitCloneFailed {
|
||||
self.emit(SandboxEvent::GitCloneFailed {
|
||||
url: url.clone(),
|
||||
error: e.clone(),
|
||||
});
|
||||
let duration_ms =
|
||||
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::InitializeFailed {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::InitializeFailed {
|
||||
provider: "daytona".into(),
|
||||
error: e.clone(),
|
||||
duration_ms,
|
||||
});
|
||||
|
|
@ -371,14 +363,14 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
|||
let git_svc = match git_svc {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
self.emit(ExecutionEnvEvent::GitCloneFailed {
|
||||
self.emit(SandboxEvent::GitCloneFailed {
|
||||
url: url.clone(),
|
||||
error: e.clone(),
|
||||
});
|
||||
let duration_ms =
|
||||
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::InitializeFailed {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::InitializeFailed {
|
||||
provider: "daytona".into(),
|
||||
error: e.clone(),
|
||||
duration_ms,
|
||||
});
|
||||
|
|
@ -402,21 +394,21 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
|||
Ok(()) => {
|
||||
let clone_duration =
|
||||
u64::try_from(clone_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::GitCloneCompleted {
|
||||
self.emit(SandboxEvent::GitCloneCompleted {
|
||||
url,
|
||||
duration_ms: clone_duration,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
let err = format!("Failed to clone repo into Daytona sandbox: {e}");
|
||||
self.emit(ExecutionEnvEvent::GitCloneFailed {
|
||||
self.emit(SandboxEvent::GitCloneFailed {
|
||||
url,
|
||||
error: err.clone(),
|
||||
});
|
||||
let duration_ms =
|
||||
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::InitializeFailed {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::InitializeFailed {
|
||||
provider: "daytona".into(),
|
||||
error: err.clone(),
|
||||
duration_ms,
|
||||
});
|
||||
|
|
@ -444,8 +436,8 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
|||
tracing::info!("Daytona sandbox ready");
|
||||
|
||||
let init_duration = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::Ready {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::Ready {
|
||||
provider: "daytona".into(),
|
||||
duration_ms: init_duration,
|
||||
});
|
||||
|
||||
|
|
@ -453,24 +445,24 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
|||
}
|
||||
|
||||
async fn cleanup(&self) -> Result<(), String> {
|
||||
self.emit(ExecutionEnvEvent::CleanupStarted {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::CleanupStarted {
|
||||
provider: "daytona".into(),
|
||||
});
|
||||
let start = Instant::now();
|
||||
if let Some(sandbox) = self.sandbox.get() {
|
||||
tracing::info!("Destroying Daytona sandbox");
|
||||
if let Err(e) = sandbox.delete().await {
|
||||
let err = format!("Failed to delete Daytona sandbox: {e}");
|
||||
self.emit(ExecutionEnvEvent::CleanupFailed {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::CleanupFailed {
|
||||
provider: "daytona".into(),
|
||||
error: err.clone(),
|
||||
});
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(ExecutionEnvEvent::CleanupCompleted {
|
||||
env_type: "daytona".into(),
|
||||
self.emit(SandboxEvent::CleanupCompleted {
|
||||
provider: "daytona".into(),
|
||||
duration_ms,
|
||||
});
|
||||
Ok(())
|
||||
|
|
@ -775,8 +767,8 @@ mod tests {
|
|||
fn daytona_config_defaults() {
|
||||
let config = DaytonaConfig::default();
|
||||
assert!(config.snapshot.is_none());
|
||||
assert!(config.sandbox.auto_stop_interval.is_none());
|
||||
assert!(config.sandbox.labels.is_none());
|
||||
assert!(config.auto_stop_interval.is_none());
|
||||
assert!(config.labels.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use arc_agent::ExecutionEnvironment;
|
||||
use arc_agent::Sandbox;
|
||||
use chrono::Utc;
|
||||
use futures::FutureExt;
|
||||
use rand::Rng;
|
||||
|
|
@ -531,7 +531,7 @@ pub struct GitState {
|
|||
pub enum GitCheckpointMode {
|
||||
/// Run git commands on the host filesystem (local & Docker bind-mount).
|
||||
Host(PathBuf),
|
||||
/// Run git commands inside the remote execution environment via `exec_command`.
|
||||
/// Run git commands inside the remote sandbox via `exec_command`.
|
||||
/// The `PathBuf` is the host repo path used for `MetadataStore` (shadow commits).
|
||||
Remote(PathBuf),
|
||||
}
|
||||
|
|
@ -579,9 +579,9 @@ async fn git_diff_host(work_dir: PathBuf, base: String) -> Option<String> {
|
|||
|
||||
pub const GIT_REMOTE: &str = "git -c maintenance.auto=0 -c gc.auto=0";
|
||||
|
||||
/// Run a git checkpoint commit inside a remote execution environment.
|
||||
/// Run a git checkpoint commit inside a remote sandbox.
|
||||
pub async fn git_checkpoint_remote(
|
||||
exec_env: &dyn ExecutionEnvironment,
|
||||
sandbox: &dyn Sandbox,
|
||||
run_id: &str,
|
||||
node_id: &str,
|
||||
status: &str,
|
||||
|
|
@ -590,7 +590,7 @@ pub async fn git_checkpoint_remote(
|
|||
) -> Option<String> {
|
||||
// Stage everything
|
||||
let add_cmd = format!("{GIT_REMOTE} add -A");
|
||||
let add_result = exec_env
|
||||
let add_result = sandbox
|
||||
.exec_command(&add_cmd, 30_000, None, None, None)
|
||||
.await;
|
||||
if add_result.as_ref().map_or(true, |r| r.exit_code != 0) {
|
||||
|
|
@ -620,7 +620,7 @@ pub async fn git_checkpoint_remote(
|
|||
let message = trailerlink::format_message(&subject, "", &trailers);
|
||||
|
||||
// Write message to temp file in sandbox to avoid shell escaping issues
|
||||
if exec_env
|
||||
if sandbox
|
||||
.write_file("/tmp/arc-commit-msg", &message)
|
||||
.await
|
||||
.is_err()
|
||||
|
|
@ -632,7 +632,7 @@ pub async fn git_checkpoint_remote(
|
|||
let commit_cmd = format!(
|
||||
"{GIT_REMOTE} -c user.name=arc -c user.email=arc@local commit --allow-empty -F /tmp/arc-commit-msg"
|
||||
);
|
||||
let commit_result = exec_env
|
||||
let commit_result = sandbox
|
||||
.exec_command(&commit_cmd, 30_000, None, None, None)
|
||||
.await;
|
||||
if commit_result.as_ref().map_or(true, |r| r.exit_code != 0) {
|
||||
|
|
@ -641,7 +641,7 @@ pub async fn git_checkpoint_remote(
|
|||
|
||||
// Get the new HEAD SHA
|
||||
let sha_cmd = format!("{GIT_REMOTE} rev-parse HEAD");
|
||||
let sha_result = exec_env
|
||||
let sha_result = sandbox
|
||||
.exec_command(&sha_cmd, 10_000, None, None, None)
|
||||
.await;
|
||||
match sha_result {
|
||||
|
|
@ -650,10 +650,10 @@ pub async fn git_checkpoint_remote(
|
|||
}
|
||||
}
|
||||
|
||||
/// Run a git diff inside a remote execution environment.
|
||||
async fn git_diff_remote(exec_env: &dyn ExecutionEnvironment, base: &str) -> Option<String> {
|
||||
/// Run a git diff inside a remote sandbox.
|
||||
async fn git_diff_remote(sandbox: &dyn Sandbox, base: &str) -> Option<String> {
|
||||
let cmd = format!("{GIT_REMOTE} diff {base} HEAD");
|
||||
match exec_env.exec_command(&cmd, 30_000, None, None, None).await {
|
||||
match sandbox.exec_command(&cmd, 30_000, None, None, None).await {
|
||||
Ok(r) if r.exit_code == 0 => Some(r.stdout),
|
||||
_ => None,
|
||||
}
|
||||
|
|
@ -661,54 +661,54 @@ async fn git_diff_remote(exec_env: &dyn ExecutionEnvironment, base: &str) -> Opt
|
|||
|
||||
// --- Remote worktree helpers (for Daytona / sandbox environments) ---
|
||||
|
||||
/// Create a branch at a specific SHA inside a remote execution environment.
|
||||
/// Create a branch at a specific SHA inside a remote sandbox.
|
||||
pub async fn git_create_branch_at_remote(
|
||||
exec_env: &dyn ExecutionEnvironment,
|
||||
sandbox: &dyn Sandbox,
|
||||
name: &str,
|
||||
sha: &str,
|
||||
) -> bool {
|
||||
let cmd = format!("{GIT_REMOTE} branch --force {name} {sha}");
|
||||
matches!(
|
||||
exec_env.exec_command(&cmd, 30_000, None, None, None).await,
|
||||
sandbox.exec_command(&cmd, 30_000, None, None, None).await,
|
||||
Ok(r) if r.exit_code == 0
|
||||
)
|
||||
}
|
||||
|
||||
/// Add a git worktree inside a remote execution environment.
|
||||
/// Add a git worktree inside a remote sandbox.
|
||||
pub async fn git_add_worktree_remote(
|
||||
exec_env: &dyn ExecutionEnvironment,
|
||||
sandbox: &dyn Sandbox,
|
||||
path: &str,
|
||||
branch: &str,
|
||||
) -> bool {
|
||||
let cmd = format!("{GIT_REMOTE} worktree add {path} {branch}");
|
||||
matches!(
|
||||
exec_env.exec_command(&cmd, 30_000, None, None, None).await,
|
||||
sandbox.exec_command(&cmd, 30_000, None, None, None).await,
|
||||
Ok(r) if r.exit_code == 0
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove a git worktree inside a remote execution environment.
|
||||
pub async fn git_remove_worktree_remote(exec_env: &dyn ExecutionEnvironment, path: &str) -> bool {
|
||||
/// Remove a git worktree inside a remote sandbox.
|
||||
pub async fn git_remove_worktree_remote(sandbox: &dyn Sandbox, path: &str) -> bool {
|
||||
let cmd = format!("{GIT_REMOTE} worktree remove --force {path}");
|
||||
matches!(
|
||||
exec_env.exec_command(&cmd, 30_000, None, None, None).await,
|
||||
sandbox.exec_command(&cmd, 30_000, None, None, None).await,
|
||||
Ok(r) if r.exit_code == 0
|
||||
)
|
||||
}
|
||||
|
||||
/// Fast-forward merge to a given SHA inside a remote execution environment.
|
||||
pub async fn git_merge_ff_only_remote(exec_env: &dyn ExecutionEnvironment, sha: &str) -> bool {
|
||||
/// Fast-forward merge to a given SHA inside a remote sandbox.
|
||||
pub async fn git_merge_ff_only_remote(sandbox: &dyn Sandbox, sha: &str) -> bool {
|
||||
let cmd = format!("{GIT_REMOTE} merge --ff-only {sha}");
|
||||
matches!(
|
||||
exec_env.exec_command(&cmd, 30_000, None, None, None).await,
|
||||
sandbox.exec_command(&cmd, 30_000, None, None, None).await,
|
||||
Ok(r) if r.exit_code == 0
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the current HEAD SHA from a remote execution environment.
|
||||
pub async fn git_head_sha_remote(exec_env: &dyn ExecutionEnvironment) -> Option<String> {
|
||||
/// Get the current HEAD SHA from a remote sandbox.
|
||||
pub async fn git_head_sha_remote(sandbox: &dyn Sandbox) -> Option<String> {
|
||||
let cmd = format!("{GIT_REMOTE} rev-parse HEAD");
|
||||
match exec_env.exec_command(&cmd, 10_000, None, None, None).await {
|
||||
match sandbox.exec_command(&cmd, 10_000, None, None, None).await {
|
||||
Ok(r) if r.exit_code == 0 => Some(r.stdout.trim().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
|
|
@ -716,12 +716,12 @@ pub async fn git_head_sha_remote(exec_env: &dyn ExecutionEnvironment) -> Option<
|
|||
|
||||
/// Remove any stale worktree at `path` (best-effort), then add a fresh one.
|
||||
pub async fn git_replace_worktree_remote(
|
||||
exec_env: &dyn ExecutionEnvironment,
|
||||
sandbox: &dyn Sandbox,
|
||||
path: &str,
|
||||
branch: &str,
|
||||
) -> bool {
|
||||
let _ = git_remove_worktree_remote(exec_env, path).await;
|
||||
git_add_worktree_remote(exec_env, path, branch).await
|
||||
let _ = git_remove_worktree_remote(sandbox, path).await;
|
||||
git_add_worktree_remote(sandbox, path, branch).await
|
||||
}
|
||||
|
||||
/// Configuration for a workflow run.
|
||||
|
|
@ -754,13 +754,13 @@ impl WorkflowRunEngine {
|
|||
pub fn new(
|
||||
registry: HandlerRegistry,
|
||||
emitter: Arc<EventEmitter>,
|
||||
execution_env: Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
) -> Self {
|
||||
Self {
|
||||
services: EngineServices {
|
||||
registry: Arc::new(registry),
|
||||
emitter,
|
||||
execution_env,
|
||||
sandbox,
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
},
|
||||
interviewer: None,
|
||||
|
|
@ -774,7 +774,7 @@ impl WorkflowRunEngine {
|
|||
services: EngineServices {
|
||||
registry: Arc::clone(&services.registry),
|
||||
emitter: Arc::clone(&services.emitter),
|
||||
execution_env: Arc::clone(&services.execution_env),
|
||||
sandbox: Arc::clone(&services.sandbox),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
},
|
||||
interviewer: None,
|
||||
|
|
@ -787,13 +787,13 @@ impl WorkflowRunEngine {
|
|||
registry: HandlerRegistry,
|
||||
emitter: Arc<EventEmitter>,
|
||||
interviewer: Arc<dyn Interviewer>,
|
||||
execution_env: Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
) -> Self {
|
||||
Self {
|
||||
services: EngineServices {
|
||||
registry: Arc::new(registry),
|
||||
emitter,
|
||||
execution_env,
|
||||
sandbox,
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
},
|
||||
interviewer: Some(interviewer),
|
||||
|
|
@ -1400,9 +1400,9 @@ impl WorkflowRunEngine {
|
|||
context.append_log(format!("artifact offload failed: {e}"));
|
||||
}
|
||||
|
||||
// Sync artifact files to the execution environment (no-op for local envs)
|
||||
// Sync artifact files to the sandbox (no-op for local envs)
|
||||
if let Err(e) =
|
||||
sync_artifacts_to_env(&mut outcome.context_updates, &*self.services.execution_env)
|
||||
sync_artifacts_to_env(&mut outcome.context_updates, &*self.services.sandbox)
|
||||
.await
|
||||
{
|
||||
context.append_log(format!("artifact sync failed: {e}"));
|
||||
|
|
@ -1523,7 +1523,7 @@ impl WorkflowRunEngine {
|
|||
}
|
||||
GitCheckpointMode::Remote(_) => {
|
||||
git_checkpoint_remote(
|
||||
&*self.services.execution_env,
|
||||
&*self.services.sandbox,
|
||||
&run_id,
|
||||
&node.id,
|
||||
&outcome.status.to_string(),
|
||||
|
|
@ -1559,7 +1559,7 @@ impl WorkflowRunEngine {
|
|||
git_diff_host(work_dir.clone(), diff_base).await
|
||||
}
|
||||
GitCheckpointMode::Remote(_) => {
|
||||
git_diff_remote(&*self.services.execution_env, &diff_base).await
|
||||
git_diff_remote(&*self.services.sandbox, &diff_base).await
|
||||
}
|
||||
};
|
||||
if let Some(patch) = diff_result {
|
||||
|
|
@ -1677,7 +1677,7 @@ impl WorkflowRunEngine {
|
|||
git_diff_host(work_dir.clone(), base.clone()).await
|
||||
}
|
||||
GitCheckpointMode::Remote(_) => {
|
||||
git_diff_remote(&*self.services.execution_env, base).await
|
||||
git_diff_remote(&*self.services.sandbox, base).await
|
||||
}
|
||||
};
|
||||
if let Some(patch) = patch {
|
||||
|
|
@ -1703,8 +1703,8 @@ mod tests {
|
|||
use async_trait::async_trait;
|
||||
use std::time::Duration;
|
||||
|
||||
fn local_env() -> Arc<dyn ExecutionEnvironment> {
|
||||
Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
fn local_env() -> Arc<dyn Sandbox> {
|
||||
Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicI64, Ordering};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::outcome::StageUsage;
|
||||
use arc_agent::{AgentEvent, ExecutionEnvEvent};
|
||||
use arc_agent::{AgentEvent, SandboxEvent};
|
||||
|
||||
/// Events emitted during workflow run execution for observability.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -147,9 +147,9 @@ pub enum WorkflowRunEvent {
|
|||
status: String,
|
||||
duration_ms: u64,
|
||||
},
|
||||
/// Forwarded from an execution environment lifecycle operation.
|
||||
ExecutionEnv {
|
||||
event: ExecutionEnvEvent,
|
||||
/// Forwarded from a sandbox lifecycle operation.
|
||||
Sandbox {
|
||||
event: SandboxEvent,
|
||||
},
|
||||
SetupStarted {
|
||||
command_count: usize,
|
||||
|
|
@ -364,7 +364,7 @@ impl WorkflowRunEvent {
|
|||
debug!(stage, text_len = text.len(), "Prompt sent");
|
||||
}
|
||||
Self::Agent { .. } => {}
|
||||
Self::ExecutionEnv { .. } => {}
|
||||
Self::Sandbox { .. } => {}
|
||||
Self::ParallelEarlyTermination {
|
||||
reason,
|
||||
completed_count,
|
||||
|
|
@ -970,21 +970,21 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn execution_env_event_wrapper_serialization() {
|
||||
use arc_agent::ExecutionEnvEvent;
|
||||
fn sandbox_event_wrapper_serialization() {
|
||||
use arc_agent::SandboxEvent;
|
||||
|
||||
let event = WorkflowRunEvent::ExecutionEnv {
|
||||
event: ExecutionEnvEvent::Initializing {
|
||||
env_type: "docker".into(),
|
||||
let event = WorkflowRunEvent::Sandbox {
|
||||
event: SandboxEvent::Initializing {
|
||||
provider: "docker".into(),
|
||||
},
|
||||
};
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
assert!(json.contains("ExecutionEnv"));
|
||||
assert!(json.contains("Sandbox"));
|
||||
assert!(json.contains("Initializing"));
|
||||
assert!(json.contains("docker"));
|
||||
|
||||
let deserialized: WorkflowRunEvent = serde_json::from_str(&json).unwrap();
|
||||
assert!(matches!(deserialized, WorkflowRunEvent::ExecutionEnv { .. }));
|
||||
assert!(matches!(deserialized, WorkflowRunEvent::Sandbox { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_agent::ExecutionEnvironment;
|
||||
use arc_agent::Sandbox;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::context::Context;
|
||||
|
|
@ -35,7 +35,7 @@ pub trait CodergenBackend: Send + Sync {
|
|||
thread_id: Option<&str>,
|
||||
emitter: &Arc<EventEmitter>,
|
||||
stage_dir: &Path,
|
||||
execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError>;
|
||||
|
||||
/// Run a single LLM call with no tools (one_shot mode).
|
||||
|
|
@ -249,7 +249,7 @@ impl Handler for CodergenHandler {
|
|||
thread_id.as_deref(),
|
||||
&services.emitter,
|
||||
&stage_dir,
|
||||
&services.execution_env,
|
||||
&services.sandbox,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -333,7 +333,7 @@ mod tests {
|
|||
EngineServices {
|
||||
registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
@ -582,7 +582,7 @@ mod tests {
|
|||
thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
*self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from));
|
||||
Ok(CodergenResult::Text {
|
||||
|
|
@ -633,7 +633,7 @@ mod tests {
|
|||
thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
*self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from));
|
||||
Ok(CodergenResult::Text {
|
||||
|
|
@ -679,7 +679,7 @@ mod tests {
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
Err(ArcError::Handler("Request timed out".to_string()))
|
||||
}
|
||||
|
|
@ -795,7 +795,7 @@ Some text in between.
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
panic!("run() should not be called in one_shot mode");
|
||||
}
|
||||
|
|
@ -910,7 +910,7 @@ Some text in between.
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
Err(ArcError::Validation("bad config".to_string()))
|
||||
}
|
||||
|
|
@ -948,7 +948,7 @@ Some text in between.
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &std::path::Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
*self.captured_prompt.lock().unwrap() = Some(prompt.to_string());
|
||||
Ok(CodergenResult::Text {
|
||||
|
|
@ -1016,7 +1016,7 @@ Some text in between.
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &std::path::Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
*self.captured_prompt.lock().unwrap() = Some(prompt.to_string());
|
||||
Ok(CodergenResult::Text {
|
||||
|
|
@ -1070,7 +1070,7 @@ Some text in between.
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &std::path::Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
panic!("run() should not be called in one_shot mode");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ mod tests {
|
|||
EngineServices {
|
||||
registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ mod tests {
|
|||
EngineServices {
|
||||
registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_agent::ExecutionEnvironment;
|
||||
use arc_agent::Sandbox;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::context::Context;
|
||||
|
|
@ -51,7 +51,7 @@ impl Handler for FanInHandler {
|
|||
logs_root,
|
||||
&node.id,
|
||||
&services.emitter,
|
||||
&services.execution_env,
|
||||
&services.sandbox,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
|
|
@ -90,7 +90,7 @@ impl Handler for FanInHandler {
|
|||
.await;
|
||||
}
|
||||
crate::engine::GitCheckpointMode::Remote(_) => {
|
||||
crate::engine::git_merge_ff_only_remote(&*services.execution_env, sha).await;
|
||||
crate::engine::git_merge_ff_only_remote(&*services.sandbox, sha).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -196,7 +196,7 @@ async fn llm_evaluate(
|
|||
logs_root: &Path,
|
||||
node_id: &str,
|
||||
emitter: &Arc<EventEmitter>,
|
||||
execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<Candidate, ArcError> {
|
||||
let results_text =
|
||||
serde_json::to_string_pretty(results).unwrap_or_else(|_| results.to_string());
|
||||
|
|
@ -224,7 +224,7 @@ async fn llm_evaluate(
|
|||
None,
|
||||
emitter,
|
||||
&stage_dir,
|
||||
execution_env,
|
||||
sandbox,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
@ -299,7 +299,7 @@ mod tests {
|
|||
EngineServices {
|
||||
registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
@ -429,7 +429,7 @@ mod tests {
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &std::path::Path,
|
||||
_execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
// Return text that contains the ID "branch_b"
|
||||
Ok(CodergenResult::Text {
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ mod tests {
|
|||
EngineServices {
|
||||
registry: std::sync::Arc::new(registry),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
@ -364,7 +364,7 @@ mod tests {
|
|||
let services = EngineServices {
|
||||
registry: std::sync::Arc::new(registry),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
@ -471,7 +471,7 @@ mod tests {
|
|||
let services = EngineServices {
|
||||
registry: std::sync::Arc::new(registry),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
@ -534,7 +534,7 @@ mod tests {
|
|||
let services = EngineServices {
|
||||
registry: std::sync::Arc::new(registry),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use std::collections::HashMap;
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_agent::ExecutionEnvironment;
|
||||
use arc_agent::Sandbox;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::context::Context;
|
||||
|
|
@ -27,7 +27,7 @@ use crate::outcome::Outcome;
|
|||
pub struct EngineServices {
|
||||
pub registry: Arc<HandlerRegistry>,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
pub execution_env: Arc<dyn ExecutionEnvironment>,
|
||||
pub sandbox: Arc<dyn Sandbox>,
|
||||
/// Git state for the current run. Set via `set_git_state` at the start of
|
||||
/// `run_internal` and read by parallel/fan-in handlers.
|
||||
pub(crate) git_state: std::sync::RwLock<Option<Arc<GitState>>>,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use arc_agent::ExecutionEnvironment;
|
||||
use arc_agent::Sandbox;
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
|
|
@ -16,18 +16,18 @@ use crate::outcome::{Outcome, StageStatus};
|
|||
use super::{EngineServices, Handler};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WorktreeEnv — decorates an ExecutionEnvironment with a custom working dir
|
||||
// WorktreeSandbox — decorates an Sandbox with a custom working dir
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Wraps an existing `ExecutionEnvironment` so that all operations use a
|
||||
/// Wraps an existing `Sandbox` so that all operations use a
|
||||
/// different working directory (the worktree path inside a remote sandbox).
|
||||
struct WorktreeEnv {
|
||||
inner: Arc<dyn ExecutionEnvironment>,
|
||||
struct WorktreeSandbox {
|
||||
inner: Arc<dyn Sandbox>,
|
||||
worktree_dir: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionEnvironment for WorktreeEnv {
|
||||
impl Sandbox for WorktreeSandbox {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &str,
|
||||
|
|
@ -49,7 +49,7 @@ impl ExecutionEnvironment for WorktreeEnv {
|
|||
&self,
|
||||
path: &str,
|
||||
depth: Option<usize>,
|
||||
) -> Result<Vec<arc_agent::execution_env::DirEntry>, String> {
|
||||
) -> Result<Vec<arc_agent::sandbox::DirEntry>, String> {
|
||||
self.inner.list_directory(path, depth).await
|
||||
}
|
||||
async fn exec_command(
|
||||
|
|
@ -59,7 +59,7 @@ impl ExecutionEnvironment for WorktreeEnv {
|
|||
working_dir: Option<&str>,
|
||||
env_vars: Option<&std::collections::HashMap<String, String>>,
|
||||
cancel_token: Option<tokio_util::sync::CancellationToken>,
|
||||
) -> Result<arc_agent::execution_env::ExecResult, String> {
|
||||
) -> Result<arc_agent::sandbox::ExecResult, String> {
|
||||
// Default to worktree dir when no explicit working_dir is given
|
||||
let wd = working_dir.unwrap_or(&self.worktree_dir);
|
||||
self.inner
|
||||
|
|
@ -70,7 +70,7 @@ impl ExecutionEnvironment for WorktreeEnv {
|
|||
&self,
|
||||
pattern: &str,
|
||||
path: &str,
|
||||
options: &arc_agent::execution_env::GrepOptions,
|
||||
options: &arc_agent::sandbox::GrepOptions,
|
||||
) -> Result<Vec<String>, String> {
|
||||
self.inner.grep(pattern, path, options).await
|
||||
}
|
||||
|
|
@ -242,7 +242,7 @@ impl Handler for ParallelHandler {
|
|||
}
|
||||
GitCheckpointMode::Remote(_) => {
|
||||
crate::engine::git_checkpoint_remote(
|
||||
&*services.execution_env,
|
||||
&*services.sandbox,
|
||||
&gs.run_id,
|
||||
&node.id,
|
||||
"parallel_base",
|
||||
|
|
@ -256,12 +256,12 @@ impl Handler for ParallelHandler {
|
|||
None
|
||||
};
|
||||
|
||||
// Build per-branch execution environments (sequentially for git setup)
|
||||
// Build per-branch sandboxs (sequentially for git setup)
|
||||
struct BranchSetup {
|
||||
target_id: String,
|
||||
branch_index: usize,
|
||||
branch_context: Context,
|
||||
execution_env: Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
worktree_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +270,7 @@ impl Handler for ParallelHandler {
|
|||
let target_id = edge.to.clone();
|
||||
let branch_context = context.clone_context();
|
||||
|
||||
let (branch_exec_env, worktree_path): (Arc<dyn ExecutionEnvironment>, Option<PathBuf>) =
|
||||
let (branch_sandbox, worktree_path): (Arc<dyn Sandbox>, Option<PathBuf>) =
|
||||
if let (Some(ref gs), Some(ref bsha)) = (&git_state, &base_sha) {
|
||||
let branch_key = &target_id;
|
||||
let branch_name = format!(
|
||||
|
|
@ -305,21 +305,21 @@ impl Handler for ParallelHandler {
|
|||
"internal.work_dir",
|
||||
serde_json::json!(wt_path.to_string_lossy().as_ref()),
|
||||
);
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(
|
||||
arc_agent::LocalExecutionEnvironment::new(wt_path.clone()),
|
||||
let env: Arc<dyn Sandbox> = Arc::new(
|
||||
arc_agent::LocalSandbox::new(wt_path.clone()),
|
||||
);
|
||||
(env, Some(wt_path))
|
||||
}
|
||||
GitCheckpointMode::Remote(_) => {
|
||||
let wt_path_str = format!(
|
||||
"{}/.arc/logs/{}/parallel/{}/{}",
|
||||
services.execution_env.working_directory(),
|
||||
services.sandbox.working_directory(),
|
||||
gs.run_id,
|
||||
node.id,
|
||||
branch_key
|
||||
);
|
||||
let ok = crate::engine::git_create_branch_at_remote(
|
||||
&*services.execution_env,
|
||||
&*services.sandbox,
|
||||
&branch_name,
|
||||
bsha,
|
||||
)
|
||||
|
|
@ -330,7 +330,7 @@ impl Handler for ParallelHandler {
|
|||
)));
|
||||
}
|
||||
let ok = crate::engine::git_replace_worktree_remote(
|
||||
&*services.execution_env,
|
||||
&*services.sandbox,
|
||||
&wt_path_str,
|
||||
&branch_name,
|
||||
)
|
||||
|
|
@ -344,7 +344,7 @@ impl Handler for ParallelHandler {
|
|||
let reset_cmd =
|
||||
format!("{} reset --hard {bsha}", crate::engine::GIT_REMOTE);
|
||||
let reset_result = services
|
||||
.execution_env
|
||||
.sandbox
|
||||
.exec_command(&reset_cmd, 30_000, Some(&wt_path_str), None, None)
|
||||
.await;
|
||||
if !matches!(reset_result, Ok(ref r) if r.exit_code == 0) {
|
||||
|
|
@ -354,22 +354,22 @@ impl Handler for ParallelHandler {
|
|||
}
|
||||
branch_context
|
||||
.set("internal.work_dir", serde_json::json!(&wt_path_str));
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(WorktreeEnv {
|
||||
inner: Arc::clone(&services.execution_env),
|
||||
let env: Arc<dyn Sandbox> = Arc::new(WorktreeSandbox {
|
||||
inner: Arc::clone(&services.sandbox),
|
||||
worktree_dir: wt_path_str.clone(),
|
||||
});
|
||||
(env, Some(PathBuf::from(wt_path_str)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(Arc::clone(&services.execution_env), None)
|
||||
(Arc::clone(&services.sandbox), None)
|
||||
};
|
||||
|
||||
branch_setups.push(BranchSetup {
|
||||
target_id,
|
||||
branch_index,
|
||||
branch_context,
|
||||
execution_env: branch_exec_env,
|
||||
sandbox: branch_sandbox,
|
||||
worktree_path,
|
||||
});
|
||||
}
|
||||
|
|
@ -417,7 +417,7 @@ impl Handler for ParallelHandler {
|
|||
let branch_services = EngineServices {
|
||||
registry: Arc::clone(®istry),
|
||||
emitter: Arc::clone(&emitter),
|
||||
execution_env: Arc::clone(&setup.execution_env),
|
||||
sandbox: Arc::clone(&setup.sandbox),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
};
|
||||
let handler = registry.resolve(target_node);
|
||||
|
|
@ -440,7 +440,7 @@ impl Handler for ParallelHandler {
|
|||
let git_r = crate::engine::GIT_REMOTE;
|
||||
let add_cmd = format!("{git_r} add -A");
|
||||
let add_result = setup
|
||||
.execution_env
|
||||
.sandbox
|
||||
.exec_command(&add_cmd, 30_000, None, None, None)
|
||||
.await;
|
||||
if add_result.as_ref().is_ok_and(|r| r.exit_code == 0) {
|
||||
|
|
@ -449,13 +449,13 @@ impl Handler for ParallelHandler {
|
|||
"{git_r} -c user.name=arc -c user.email=arc@local commit --allow-empty -m '{msg}'"
|
||||
);
|
||||
let _ = setup
|
||||
.execution_env
|
||||
.sandbox
|
||||
.exec_command(&commit_cmd, 30_000, None, None, None)
|
||||
.await;
|
||||
}
|
||||
let sha_cmd = format!("{git_r} rev-parse HEAD");
|
||||
let sha_result = setup
|
||||
.execution_env
|
||||
.sandbox
|
||||
.exec_command(&sha_cmd, 10_000, None, None, None)
|
||||
.await;
|
||||
match sha_result {
|
||||
|
|
@ -564,7 +564,7 @@ impl Handler for ParallelHandler {
|
|||
GitCheckpointMode::Remote(_) => {
|
||||
let wt_str = wt_path.to_string_lossy().to_string();
|
||||
crate::engine::git_remove_worktree_remote(
|
||||
&*services.execution_env,
|
||||
&*services.sandbox,
|
||||
&wt_str,
|
||||
)
|
||||
.await;
|
||||
|
|
@ -592,7 +592,7 @@ impl Handler for ParallelHandler {
|
|||
.await;
|
||||
}
|
||||
GitCheckpointMode::Remote(_) => {
|
||||
crate::engine::git_merge_ff_only_remote(&*services.execution_env, sha)
|
||||
crate::engine::git_merge_ff_only_remote(&*services.sandbox, sha)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
|
@ -717,7 +717,7 @@ mod tests {
|
|||
EngineServices {
|
||||
registry: Arc::new(registry),
|
||||
emitter: Arc::new(EventEmitter::new()),
|
||||
execution_env: Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ mod tests {
|
|||
EngineServices {
|
||||
registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ mod tests {
|
|||
EngineServices {
|
||||
registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ mod tests {
|
|||
EngineServices {
|
||||
registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))),
|
||||
emitter: std::sync::Arc::new(EventEmitter::new()),
|
||||
execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ pub mod checkpoint;
|
|||
pub mod cli;
|
||||
pub mod condition;
|
||||
pub mod context;
|
||||
pub mod daytona_env;
|
||||
pub mod daytona_sandbox;
|
||||
pub mod engine;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex};
|
|||
use std::time::Duration;
|
||||
|
||||
use arc_agent::{
|
||||
AnthropicProfile, ExecutionEnvironment, GeminiProfile, OpenAiProfile, ProviderProfile, Session,
|
||||
AnthropicProfile, Sandbox, GeminiProfile, OpenAiProfile, ProviderProfile, Session,
|
||||
SessionConfig,
|
||||
};
|
||||
use arc_llm::client::Client;
|
||||
|
|
@ -112,16 +112,16 @@ const SUBMIT_RETRO_SCHEMA: &str = r#"{
|
|||
/// a structured narrative. The agent explores `progress.ndjson` and other
|
||||
/// files via tool access, then calls `submit_retro` with its analysis.
|
||||
pub async fn run_retro_agent(
|
||||
execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
logs_root: &Path,
|
||||
llm_client: &Client,
|
||||
provider: Provider,
|
||||
model: &str,
|
||||
) -> anyhow::Result<RetroNarrative> {
|
||||
// Upload data files into execution env (needed for Daytona; no-op effect for local
|
||||
// Upload data files into sandbox (needed for Daytona; no-op effect for local
|
||||
// since the agent can also read from the original paths via tools).
|
||||
let retro_data_dir = "/tmp/retro_data";
|
||||
upload_data_files(execution_env, logs_root, retro_data_dir).await?;
|
||||
upload_data_files(sandbox, logs_root, retro_data_dir).await?;
|
||||
|
||||
// Build provider profile with the submit_retro tool
|
||||
let captured: Arc<Mutex<Option<RetroNarrative>>> = Arc::new(Mutex::new(None));
|
||||
|
|
@ -164,7 +164,7 @@ pub async fn run_retro_agent(
|
|||
let mut session = Session::new(
|
||||
llm_client.clone(),
|
||||
profile,
|
||||
Arc::clone(execution_env),
|
||||
Arc::clone(sandbox),
|
||||
config,
|
||||
);
|
||||
|
||||
|
|
@ -218,12 +218,12 @@ fn build_profile(provider: Provider, model: &str) -> Box<dyn ProviderProfile> {
|
|||
}
|
||||
|
||||
async fn upload_data_files(
|
||||
execution_env: &Arc<dyn ExecutionEnvironment>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
logs_root: &Path,
|
||||
target_dir: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
// Create target directory
|
||||
execution_env
|
||||
sandbox
|
||||
.exec_command(&format!("mkdir -p {target_dir}"), 10_000, None, None, None)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create retro data dir: {e}"))?;
|
||||
|
|
@ -233,7 +233,7 @@ async fn upload_data_files(
|
|||
let source = logs_root.join(filename);
|
||||
if source.exists() {
|
||||
let content = std::fs::read_to_string(&source)?;
|
||||
execution_env
|
||||
sandbox
|
||||
.write_file(&format!("{target_dir}/{filename}"), &content)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to upload {filename}: {e}"))?;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Integration tests for `DaytonaExecutionEnvironment`.
|
||||
//! Integration tests for `DaytonaSandbox`.
|
||||
//!
|
||||
//! These tests require a `DAYTONA_API_KEY` environment variable and network access.
|
||||
//! Run with: `cargo test --package arc-workflows -- --ignored daytona`
|
||||
|
|
@ -7,12 +7,12 @@ use std::collections::HashMap;
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_agent::ExecutionEnvironment;
|
||||
use arc_agent::Sandbox;
|
||||
use arc_llm::provider::Provider;
|
||||
use arc_workflows::artifact::sync_artifacts_to_env;
|
||||
use arc_workflows::checkpoint::Checkpoint;
|
||||
use arc_workflows::context::Context;
|
||||
use arc_workflows::daytona_env::{DaytonaConfig, DaytonaExecutionEnvironment};
|
||||
use arc_workflows::daytona_sandbox::{DaytonaConfig, DaytonaSandbox};
|
||||
use arc_workflows::engine::{WorkflowRunEngine, RunConfig};
|
||||
use arc_workflows::error::ArcError;
|
||||
use arc_workflows::event::EventEmitter;
|
||||
|
|
@ -22,12 +22,12 @@ use arc_workflows::handler::start::StartHandler;
|
|||
use arc_workflows::handler::{Handler, HandlerRegistry};
|
||||
use arc_workflows::outcome::{Outcome, StageStatus};
|
||||
|
||||
async fn create_env() -> DaytonaExecutionEnvironment {
|
||||
async fn create_env() -> DaytonaSandbox {
|
||||
dotenvy::dotenv().ok();
|
||||
let client = daytona_sdk::Client::new()
|
||||
.await
|
||||
.expect("Failed to create Daytona client — is DAYTONA_API_KEY set?");
|
||||
DaytonaExecutionEnvironment::new(client, DaytonaConfig::default())
|
||||
DaytonaSandbox::new(client, DaytonaConfig::default())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -117,7 +117,7 @@ async fn daytona_full_lifecycle() {
|
|||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn daytona_snapshot_sandbox() {
|
||||
use arc_workflows::daytona_env::{DaytonaSandboxConfig, DaytonaSnapshotConfig};
|
||||
use arc_workflows::daytona_sandbox::DaytonaSnapshotConfig;
|
||||
|
||||
dotenvy::dotenv().ok();
|
||||
let client = daytona_sdk::Client::new()
|
||||
|
|
@ -125,10 +125,8 @@ async fn daytona_snapshot_sandbox() {
|
|||
.expect("Failed to create Daytona client — is DAYTONA_API_KEY set?");
|
||||
|
||||
let config = DaytonaConfig {
|
||||
sandbox: DaytonaSandboxConfig {
|
||||
auto_stop_interval: Some(60),
|
||||
..Default::default()
|
||||
},
|
||||
auto_stop_interval: Some(60),
|
||||
labels: None,
|
||||
snapshot: Some(DaytonaSnapshotConfig {
|
||||
name: "arc-test-snapshot".to_string(),
|
||||
cpu: Some(2),
|
||||
|
|
@ -140,7 +138,7 @@ async fn daytona_snapshot_sandbox() {
|
|||
}),
|
||||
};
|
||||
|
||||
let env = DaytonaExecutionEnvironment::new(client, config);
|
||||
let env = DaytonaSandbox::new(client, config);
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
// Verify rg is available (installed by snapshot)
|
||||
|
|
@ -236,7 +234,7 @@ impl Handler for LargeOutputHandler {
|
|||
async fn daytona_pipeline_artifact_offload_and_sync() {
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(env);
|
||||
let env: Arc<dyn Sandbox> = Arc::new(env);
|
||||
|
||||
// Pipeline: start -> big_output -> exit
|
||||
let mut graph = Graph::new("DaytonaArtifactPipeline");
|
||||
|
|
@ -350,7 +348,7 @@ impl Handler for FileWriterHandler {
|
|||
let content = format!("output from {}", node.id);
|
||||
let cmd = format!("echo '{content}' > {}.txt", node.id);
|
||||
let _ = services
|
||||
.execution_env
|
||||
.sandbox
|
||||
.exec_command(&cmd, 10_000, None, None, None)
|
||||
.await;
|
||||
Ok(Outcome::success())
|
||||
|
|
@ -359,9 +357,9 @@ impl Handler for FileWriterHandler {
|
|||
|
||||
/// Set up git inside a Daytona sandbox for checkpoint commits.
|
||||
/// Returns (run_id, base_sha, branch_name) on success.
|
||||
async fn setup_daytona_git(exec_env: &dyn ExecutionEnvironment) -> (String, String, String) {
|
||||
async fn setup_daytona_git(sandbox: &dyn Sandbox) -> (String, String, String) {
|
||||
// Get current HEAD as base SHA
|
||||
let sha_result = exec_env
|
||||
let sha_result = sandbox
|
||||
.exec_command("git rev-parse HEAD", 10_000, None, None, None)
|
||||
.await
|
||||
.expect("git rev-parse HEAD should succeed");
|
||||
|
|
@ -376,7 +374,7 @@ async fn setup_daytona_git(exec_env: &dyn ExecutionEnvironment) -> (String, Stri
|
|||
let branch_name = format!("arc/run/{run_id}");
|
||||
|
||||
let checkout_cmd = format!("git checkout -b {branch_name}");
|
||||
let checkout_result = exec_env
|
||||
let checkout_result = sandbox
|
||||
.exec_command(&checkout_cmd, 10_000, None, None, None)
|
||||
.await
|
||||
.expect("git checkout should succeed");
|
||||
|
|
@ -394,7 +392,7 @@ async fn setup_daytona_git(exec_env: &dyn ExecutionEnvironment) -> (String, Stri
|
|||
async fn daytona_git_checkpoint_remote_emits_events() {
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(env);
|
||||
let env: Arc<dyn Sandbox> = Arc::new(env);
|
||||
|
||||
// Install git if not available (the default ubuntu:22.04 image may not have it)
|
||||
let git_check = env
|
||||
|
|
@ -553,7 +551,7 @@ use arc_workflows::handler::parallel::ParallelHandler;
|
|||
async fn daytona_parallel_git_branching_e2e() {
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(env);
|
||||
let env: Arc<dyn Sandbox> = Arc::new(env);
|
||||
|
||||
// Install git if not available
|
||||
let git_check = env
|
||||
|
|
@ -781,7 +779,7 @@ use arc_workflows::handler::codergen::{CodergenBackend, CodergenResult};
|
|||
async fn run_daytona_cli_test(provider: Provider, model: &str, install_command: &str) {
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(env);
|
||||
let env: Arc<dyn Sandbox> = Arc::new(env);
|
||||
|
||||
// Install the CLI tool inside the Daytona sandbox
|
||||
let install_result = env
|
||||
|
|
@ -883,7 +881,7 @@ use arc_workflows::git::MetadataStore;
|
|||
async fn daytona_git_checkpoint_with_shadow_branch() {
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(env);
|
||||
let env: Arc<dyn Sandbox> = Arc::new(env);
|
||||
|
||||
// Install git if not available
|
||||
let git_check = env
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ use arc_workflows::transform::{
|
|||
};
|
||||
use arc_workflows::validation::{validate, validate_or_raise, Severity};
|
||||
|
||||
fn local_env() -> Arc<dyn arc_agent::ExecutionEnvironment> {
|
||||
Arc::new(arc_agent::LocalExecutionEnvironment::new(
|
||||
fn local_env() -> Arc<dyn arc_agent::Sandbox> {
|
||||
Arc::new(arc_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
))
|
||||
}
|
||||
|
|
@ -1141,7 +1141,7 @@ impl CodergenBackend for MockCodergenBackend {
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &std::path::Path,
|
||||
_execution_env: &Arc<dyn arc_agent::ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn arc_agent::Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
Ok(CodergenResult::Text {
|
||||
text: format!(
|
||||
|
|
@ -5497,7 +5497,7 @@ mod real_llm {
|
|||
_thread_id: Option<&str>,
|
||||
_emitter: &Arc<EventEmitter>,
|
||||
_stage_dir: &std::path::Path,
|
||||
_execution_env: &Arc<dyn arc_agent::ExecutionEnvironment>,
|
||||
_sandbox: &Arc<dyn arc_agent::Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
let request = Request {
|
||||
model: self.model.clone(),
|
||||
|
|
@ -7628,7 +7628,7 @@ impl RemoteMockEnv {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl arc_agent::ExecutionEnvironment for RemoteMockEnv {
|
||||
impl arc_agent::Sandbox for RemoteMockEnv {
|
||||
async fn read_file(
|
||||
&self,
|
||||
_path: &str,
|
||||
|
|
@ -7712,7 +7712,7 @@ impl arc_agent::ExecutionEnvironment for RemoteMockEnv {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn artifact_pointers_rewritten_for_remote_execution_env() {
|
||||
async fn artifact_pointers_rewritten_for_remote_sandbox() {
|
||||
// Pipeline: start -> big_output -> exit
|
||||
// big_output uses LargeOutputHandler which returns a >100KB context_update.
|
||||
// RemoteMockEnv simulates a container where local files don't exist.
|
||||
|
|
@ -7966,7 +7966,7 @@ impl CliTestEnv {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl arc_agent::ExecutionEnvironment for CliTestEnv {
|
||||
impl arc_agent::Sandbox for CliTestEnv {
|
||||
async fn read_file(
|
||||
&self,
|
||||
_path: &str,
|
||||
|
|
@ -8074,13 +8074,13 @@ impl arc_agent::ExecutionEnvironment for CliTestEnv {
|
|||
}
|
||||
}
|
||||
|
||||
// -- Cycle 8: AgentCliBackend::run() e2e via mock ExecutionEnvironment --
|
||||
// -- Cycle 8: AgentCliBackend::run() e2e via mock Sandbox --
|
||||
|
||||
#[tokio::test]
|
||||
async fn cli_backend_run_writes_prompt_and_calls_exec() {
|
||||
let claude_output = r#"{"type":"result","result":"I fixed the bug.","usage":{"input_tokens":500,"output_tokens":200}}"#;
|
||||
let test_env = Arc::new(CliTestEnv::new(claude_output));
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = test_env.clone();
|
||||
let env: Arc<dyn arc_agent::Sandbox> = test_env.clone();
|
||||
let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic);
|
||||
|
||||
let node = Node::new("fix_code");
|
||||
|
|
@ -8147,7 +8147,7 @@ async fn cli_backend_run_writes_prompt_and_calls_exec() {
|
|||
#[tokio::test]
|
||||
async fn cli_backend_run_detects_changed_files() {
|
||||
let claude_output = r#"{"type":"result","result":"Created new file.","usage":{"input_tokens":100,"output_tokens":50}}"#;
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> =
|
||||
let env: Arc<dyn arc_agent::Sandbox> =
|
||||
Arc::new(CliTestEnv::new(claude_output).with_git_diff_after("src/main.rs\nsrc/lib.rs\n"));
|
||||
let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic);
|
||||
|
||||
|
|
@ -8181,7 +8181,7 @@ async fn cli_backend_run_detects_changed_files() {
|
|||
async fn cli_backend_run_with_codex_provider() {
|
||||
let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"Implemented the feature.\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":300,\"output_tokens\":150}}";
|
||||
let test_env = Arc::new(CliTestEnv::new(codex_output));
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = test_env.clone();
|
||||
let env: Arc<dyn arc_agent::Sandbox> = test_env.clone();
|
||||
let backend = AgentCliBackend::new("gpt-5.3-codex".into(), Provider::OpenAi);
|
||||
|
||||
let node = Node::new("implement");
|
||||
|
|
@ -8232,7 +8232,7 @@ async fn cli_backend_run_fails_on_nonzero_exit() {
|
|||
// Override exec_command to return non-zero for the CLI call
|
||||
struct FailingCliEnv;
|
||||
#[async_trait::async_trait]
|
||||
impl arc_agent::ExecutionEnvironment for FailingCliEnv {
|
||||
impl arc_agent::Sandbox for FailingCliEnv {
|
||||
async fn read_file(
|
||||
&self,
|
||||
_: &str,
|
||||
|
|
@ -8310,7 +8310,7 @@ async fn cli_backend_run_fails_on_nonzero_exit() {
|
|||
}
|
||||
}
|
||||
|
||||
let failing_env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(FailingCliEnv);
|
||||
let failing_env: Arc<dyn arc_agent::Sandbox> = Arc::new(FailingCliEnv);
|
||||
let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic);
|
||||
let node = Node::new("step");
|
||||
let context = Context::new();
|
||||
|
|
@ -8348,7 +8348,7 @@ async fn cli_backend_run_fails_on_nonzero_exit() {
|
|||
|
||||
#[tokio::test]
|
||||
async fn cli_backend_run_fails_on_unparseable_output() {
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> =
|
||||
let env: Arc<dyn arc_agent::Sandbox> =
|
||||
Arc::new(CliTestEnv::new("this is not json at all"));
|
||||
let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic);
|
||||
|
||||
|
|
@ -8385,7 +8385,7 @@ async fn cli_backend_run_uses_node_model_override() {
|
|||
let claude_output =
|
||||
r#"{"type":"result","result":"ok","usage":{"input_tokens":10,"output_tokens":5}}"#;
|
||||
let test_env = Arc::new(CliTestEnv::new(claude_output));
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = test_env.clone();
|
||||
let env: Arc<dyn arc_agent::Sandbox> = test_env.clone();
|
||||
let backend = AgentCliBackend::new("default-model".into(), Provider::Anthropic);
|
||||
|
||||
let mut node = Node::new("step");
|
||||
|
|
@ -8419,7 +8419,7 @@ async fn cli_backend_run_uses_node_model_override() {
|
|||
async fn cli_backend_run_uses_node_provider_override() {
|
||||
let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"ok\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":10,\"output_tokens\":5}}";
|
||||
let test_env = Arc::new(CliTestEnv::new(codex_output));
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = test_env.clone();
|
||||
let env: Arc<dyn arc_agent::Sandbox> = test_env.clone();
|
||||
let backend = AgentCliBackend::new("default-model".into(), Provider::Anthropic);
|
||||
|
||||
let mut node = Node::new("step");
|
||||
|
|
@ -8453,7 +8453,7 @@ async fn cli_backend_run_uses_node_provider_override() {
|
|||
async fn cli_backend_run_writes_provider_used_json() {
|
||||
let claude_output =
|
||||
r#"{"type":"result","result":"done","usage":{"input_tokens":10,"output_tokens":5}}"#;
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(claude_output));
|
||||
let env: Arc<dyn arc_agent::Sandbox> = Arc::new(CliTestEnv::new(claude_output));
|
||||
let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic);
|
||||
|
||||
let node = Node::new("step");
|
||||
|
|
@ -8484,7 +8484,7 @@ async fn cli_backend_run_writes_provider_used_json() {
|
|||
#[tokio::test]
|
||||
async fn backend_router_delegates_to_cli_for_cli_node() {
|
||||
let claude_output = r#"{"type":"result","result":"CLI response","usage":{"input_tokens":10,"output_tokens":5}}"#;
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(claude_output));
|
||||
let env: Arc<dyn arc_agent::Sandbox> = Arc::new(CliTestEnv::new(claude_output));
|
||||
|
||||
let api_backend = Box::new(MockCodergenBackend); // would return "Response for ..."
|
||||
let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic);
|
||||
|
|
@ -8571,7 +8571,7 @@ async fn backend_router_delegates_to_api_for_normal_node() {
|
|||
#[tokio::test]
|
||||
async fn backend_router_delegates_to_cli_for_backend_attr() {
|
||||
let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"Codex did it\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":10,\"output_tokens\":5}}";
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(codex_output));
|
||||
let env: Arc<dyn arc_agent::Sandbox> = Arc::new(CliTestEnv::new(codex_output));
|
||||
|
||||
let api_backend = Box::new(MockCodergenBackend);
|
||||
let cli = AgentCliBackend::new("gpt-5.3-codex".into(), Provider::OpenAi);
|
||||
|
|
@ -8620,7 +8620,7 @@ async fn full_pipeline_with_cli_backend_node() {
|
|||
// Pipeline: start -> api_work -> cli_work -> exit
|
||||
// api_work uses MockCodergenBackend (API), cli_work has backend="cli"
|
||||
let claude_output = r#"{"type":"result","result":"CLI completed the task.","usage":{"input_tokens":100,"output_tokens":50}}"#;
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(claude_output));
|
||||
let env: Arc<dyn arc_agent::Sandbox> = Arc::new(CliTestEnv::new(claude_output));
|
||||
|
||||
let mut graph = Graph::new("CliPipelineTest");
|
||||
|
||||
|
|
@ -8749,7 +8749,7 @@ async fn full_pipeline_with_cli_backend_node() {
|
|||
#[tokio::test]
|
||||
async fn stylesheet_backend_property_routes_to_cli() {
|
||||
let claude_output = r#"{"type":"result","result":"Styled CLI response.","usage":{"input_tokens":10,"output_tokens":5}}"#;
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(claude_output));
|
||||
let env: Arc<dyn arc_agent::Sandbox> = Arc::new(CliTestEnv::new(claude_output));
|
||||
|
||||
let mut graph = Graph::new("StylesheetTest");
|
||||
graph.attrs.insert(
|
||||
|
|
@ -8845,7 +8845,7 @@ async fn stylesheet_backend_property_routes_to_cli() {
|
|||
|
||||
use arc_workflows::cli::cli_backend::parse_cli_response;
|
||||
|
||||
/// Run a real CLI tool via LocalExecutionEnvironment and verify the full flow.
|
||||
/// Run a real CLI tool via LocalSandbox and verify the full flow.
|
||||
async fn run_real_cli_test(provider: Provider, model: &str) {
|
||||
let env = local_env();
|
||||
let backend = AgentCliBackend::new(model.to_string(), provider);
|
||||
|
|
@ -8980,10 +8980,10 @@ impl Handler for FileWriterHandler {
|
|||
_logs_root: &Path,
|
||||
services: &arc_workflows::handler::EngineServices,
|
||||
) -> Result<Outcome, ArcError> {
|
||||
let work_dir = services.execution_env.working_directory().to_string();
|
||||
let work_dir = services.sandbox.working_directory().to_string();
|
||||
let file_path = format!("{}/{}.txt", work_dir, node.id);
|
||||
services
|
||||
.execution_env
|
||||
.sandbox
|
||||
.write_file(&file_path, &format!("written by {}", node.id))
|
||||
.await
|
||||
.map_err(|e| ArcError::Handler(format!("write_file failed: {e}")))?;
|
||||
|
|
@ -9073,8 +9073,8 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() {
|
|||
let mut emitter = EventEmitter::new();
|
||||
let events = collect_events(&mut emitter);
|
||||
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(
|
||||
arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()),
|
||||
let env: Arc<dyn arc_agent::Sandbox> = Arc::new(
|
||||
arc_agent::LocalSandbox::new(worktree_path.clone()),
|
||||
);
|
||||
let mut registry = HandlerRegistry::new(Box::new(ContextSetterHandler));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
|
|
@ -9253,8 +9253,8 @@ async fn git_checkpoint_host_writes_shadow_branch() {
|
|||
std::fs::write(logs_dir.path().join("graph.dot"), "digraph {}").unwrap();
|
||||
let emitter = EventEmitter::new();
|
||||
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(
|
||||
arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()),
|
||||
let env: Arc<dyn arc_agent::Sandbox> = Arc::new(
|
||||
arc_agent::LocalSandbox::new(worktree_path.clone()),
|
||||
);
|
||||
let mut registry = HandlerRegistry::new(Box::new(ContextSetterHandler));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
|
|
@ -9440,8 +9440,8 @@ async fn parallel_git_branching_host_e2e() {
|
|||
let mut emitter = EventEmitter::new();
|
||||
let events = collect_events(&mut emitter);
|
||||
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(
|
||||
arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()),
|
||||
let env: Arc<dyn arc_agent::Sandbox> = Arc::new(
|
||||
arc_agent::LocalSandbox::new(worktree_path.clone()),
|
||||
);
|
||||
|
||||
let mut registry = HandlerRegistry::new(Box::new(FileWriterHandler));
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@ graph = "check.dot"
|
|||
[llm]
|
||||
model = "claude-sonnet"
|
||||
|
||||
[execution]
|
||||
environment = "daytona"
|
||||
[sandbox]
|
||||
provider = "daytona"
|
||||
|
||||
[execution.daytona.sandbox]
|
||||
[sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
|
||||
[execution.daytona.sandbox.labels]
|
||||
[sandbox.daytona.labels]
|
||||
project = "attractor-rust"
|
||||
|
||||
[execution.daytona.snapshot]
|
||||
[sandbox.daytona.snapshot]
|
||||
name = "attractor-rust-check-dev"
|
||||
cpu = 4
|
||||
memory = 8
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue