pub mod binding;
pub mod tools;
use crate::Runner;
use adk_core::{AdkError, AppName, Event, SessionId, UserId};
use adk_sandbox::SandboxError;
use adk_sandbox::workspace::{SandboxConfig, SnapshotId};
use serde_json::{Value, json};
use std::sync::Arc;
use futures::StreamExt;
use tracing::{info, warn};
struct SandboxToolset {
tools: Vec<Arc<dyn adk_core::Tool>>,
}
#[async_trait::async_trait]
impl adk_core::Toolset for SandboxToolset {
fn name(&self) -> &str {
"sandbox"
}
async fn tools(
&self,
_ctx: Arc<dyn adk_core::ReadonlyContext>,
) -> adk_core::Result<Vec<Arc<dyn adk_core::Tool>>> {
Ok(self.tools.clone())
}
}
fn attach_cleanup_failure(primary: &mut AdkError, stage: &'static str, cleanup: &AdkError) {
let failure = json!({
"stage": stage,
"component": cleanup.component.to_string(),
"category": cleanup.category.to_string(),
"code": cleanup.code,
"message": cleanup.message,
});
let entry = primary
.details
.metadata
.entry("sandbox.cleanupErrors".to_string())
.or_insert_with(|| Value::Array(Vec::new()));
if let Value::Array(failures) = entry {
failures.push(failure);
} else {
*entry = Value::Array(vec![failure]);
}
}
pub struct SandboxRunner {
inner: Runner,
}
impl SandboxRunner {
pub fn new(inner: Runner) -> Self {
Self { inner }
}
pub fn inner(&self) -> &Runner {
&self.inner
}
pub async fn run(
&self,
config: &SandboxConfig,
user_id: &str,
session_id: &str,
user_content: adk_core::Content,
) -> Result<SandboxRunResult, adk_core::AdkError> {
let app_name = AppName::try_from(self.inner.app_name())?;
let user_id = UserId::try_from(user_id)?;
let session_id = SessionId::try_from(session_id)?;
let get_request = adk_session::GetRequest {
app_name: app_name.to_string(),
user_id: user_id.to_string(),
session_id: session_id.to_string(),
num_recent_events: None,
after: None,
};
let create_session = match self.inner.session_service().get(get_request).await {
Ok(_) => false,
Err(error) if error.is_not_found() => true,
Err(error) => return Err(error),
};
info!("provisioning sandbox workspace");
let handle =
config.client.provision(&config.manifest).await.map_err(adk_core::AdkError::from)?;
info!(session_handle = %handle.0, "starting sandbox session");
let session = match config.client.start(&handle).await {
Ok(s) => s,
Err(e) => {
let mut primary = AdkError::from(e);
if let Err(cleanup) = config.client.stop(&handle).await {
warn!(
session_handle = %handle.0,
error = %cleanup,
"failed to stop sandbox after start failure"
);
attach_cleanup_failure(&mut primary, "stop", &AdkError::from(cleanup));
}
return Err(primary);
}
};
let session_arc = Arc::from(session);
let bound_tools =
binding::bind_tools(session_arc, &config.capabilities, config.command_timeout);
info!(
capabilities = ?config.capabilities,
tool_count = bound_tools.len(),
"bound sandbox tools"
);
let mut run_config = self.inner.run_config().clone();
run_config
.runtime_toolsets
.push(adk_core::RuntimeToolset::new(Arc::new(SandboxToolset { tools: bound_tools })));
let agent_loop_future = async {
if create_session {
self.inner
.session_service()
.create(adk_session::CreateRequest {
app_name: app_name.to_string(),
user_id: user_id.to_string(),
session_id: Some(session_id.to_string()),
state: std::collections::HashMap::new(),
})
.await?;
}
let mut events = self
.inner
.run_with_config(user_id, session_id, user_content, Some(run_config))
.await?;
let mut buffered_events = Vec::new();
while let Some(event) = events.next().await {
buffered_events.push(event?);
}
Ok::<Vec<Event>, adk_core::AdkError>(buffered_events)
};
let agent_loop_result =
tokio::time::timeout(config.session_timeout, agent_loop_future).await;
let agent_loop_result = match agent_loop_result {
Ok(result) => result,
Err(_elapsed) => {
warn!(
session_handle = %handle.0,
timeout = ?config.session_timeout,
"sandbox session timed out"
);
Err::<Vec<Event>, adk_core::AdkError>(adk_core::AdkError::from(
SandboxError::SessionTimeout { timeout: config.session_timeout },
))
}
};
let (snapshot_id, snapshot_error) = if config.snapshot_on_stop {
info!(session_handle = %handle.0, "snapshotting sandbox workspace");
match config.client.snapshot(&handle).await {
Ok(id) => {
info!(snapshot_id = %id.0, "sandbox snapshot created");
(Some(id), None)
}
Err(error) => {
warn!(
session_handle = %handle.0,
error = %error,
"failed to snapshot sandbox during cleanup"
);
(None, Some(AdkError::from(error)))
}
}
} else {
(None, None)
};
info!(session_handle = %handle.0, "stopping sandbox session");
let stop_error = match config.client.stop(&handle).await {
Ok(()) => None,
Err(error) => {
warn!(
session_handle = %handle.0,
error = %error,
"failed to stop sandbox session during cleanup"
);
Some(AdkError::from(error))
}
};
match (agent_loop_result, snapshot_error, stop_error) {
(Err(mut primary), snapshot_error, stop_error) => {
if let Some(error) = snapshot_error.as_ref() {
attach_cleanup_failure(&mut primary, "snapshot", error);
}
if let Some(error) = stop_error.as_ref() {
attach_cleanup_failure(&mut primary, "stop", error);
}
Err(primary)
}
(Ok(_), Some(mut primary), stop_error) => {
if let Some(error) = stop_error.as_ref() {
attach_cleanup_failure(&mut primary, "stop", error);
}
Err(primary)
}
(Ok(_), None, Some(primary)) => Err(primary),
(Ok(events), None, None) => Ok(SandboxRunResult { snapshot_id, events }),
}
}
}
#[derive(Debug)]
pub struct SandboxRunResult {
pub snapshot_id: Option<SnapshotId>,
pub events: Vec<Event>,
}