claw_branch/sandbox/environment.rs
1//! Isolated simulation environments backed by a forked branch.
2
3use std::sync::Arc;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use sqlx::{
8 sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
9 SqlitePool,
10};
11use uuid::Uuid;
12
13use crate::{
14 branch::lifecycle::BranchLifecycle,
15 config::BranchConfig,
16 error::BranchResult,
17 types::{Branch, SimulationOutcome},
18};
19
20/// Configures a simulation scenario run in an isolated sandbox branch.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct SimulationScenario {
23 /// A short name for the scenario (used in the branch label).
24 pub name: String,
25 /// Human-readable description of what the scenario tests.
26 pub description: String,
27 /// Maximum number of write operations before the sandbox is halted.
28 pub max_ops: Option<u32>,
29 /// Timeout in seconds after which the run is aborted.
30 pub timeout_secs: Option<u64>,
31 /// Optional JSON seed data that may be loaded into the sandbox on setup.
32 pub seed_data: Option<serde_json::Value>,
33}
34
35/// Describes the current lifecycle phase of a sandbox environment.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum SandboxStatus {
38 /// The sandbox is actively running an agent.
39 Running,
40 /// The agent finished and produced an outcome.
41 Completed {
42 /// The captured simulation result.
43 outcome: SimulationOutcome,
44 },
45 /// The run failed with an error message.
46 Failed(String),
47 /// The environment was abandoned without completing.
48 Abandoned,
49}
50
51/// An isolated, forked branch environment used to run and evaluate agent simulations.
52///
53/// # Example
54/// ```rust,ignore
55/// let env = SimulationEnvironment::setup(&parent, scenario, config, lifecycle).await?;
56/// let pool = env.branch_pool()?;
57/// // run agent against pool ...
58/// env.teardown(lifecycle).await?;
59/// ```
60#[derive(Debug, Clone)]
61pub struct SimulationEnvironment {
62 /// A unique identifier for this simulation run.
63 pub id: Uuid,
64 /// A human-readable label for the sandbox branch.
65 pub label: String,
66 /// The isolated branch backing this environment.
67 pub branch: Branch,
68 /// The parent branch from which this sandbox was forked.
69 pub parent_branch_id: Uuid,
70 /// The timestamp the environment was initialized.
71 pub started_at: DateTime<Utc>,
72 /// The scenario definition that governs this run.
73 pub scenario: SimulationScenario,
74 /// The current lifecycle status of the sandbox.
75 pub status: SandboxStatus,
76}
77
78impl SimulationEnvironment {
79 /// Forks `parent` into a new sandbox branch and returns an initialized environment.
80 ///
81 /// The branch name follows the pattern `sandbox/{scenario.name}/{id_short}`.
82 pub async fn setup(
83 parent: &Branch,
84 scenario: SimulationScenario,
85 _config: Arc<BranchConfig>,
86 lifecycle: Arc<BranchLifecycle>,
87 ) -> BranchResult<Self> {
88 let id = Uuid::new_v4();
89 let id_short = &id.to_string()[..8];
90 let branch_name = format!("sandbox/{}/{}", scenario.name, id_short);
91 let label = branch_name.clone();
92
93 let branch = lifecycle
94 .fork(parent.id, &branch_name, Some(&scenario.description))
95 .await?;
96
97 Ok(Self {
98 id,
99 label,
100 branch,
101 parent_branch_id: parent.id,
102 started_at: Utc::now(),
103 scenario,
104 status: SandboxStatus::Running,
105 })
106 }
107
108 /// Discards the sandbox branch, freeing any on-disk state.
109 ///
110 /// After calling `teardown`, the branch database is marked as `Discarded` and
111 /// will be removed on the next GC pass.
112 pub async fn teardown(&mut self, lifecycle: Arc<BranchLifecycle>) -> BranchResult<()> {
113 lifecycle.discard(self.branch.id).await?;
114 self.status = SandboxStatus::Abandoned;
115 Ok(())
116 }
117
118 /// Opens a read-write SQLite pool connected to the sandbox branch database.
119 pub async fn branch_pool(&self) -> BranchResult<SqlitePool> {
120 SqlitePoolOptions::new()
121 .max_connections(4)
122 .connect_with(
123 SqliteConnectOptions::new()
124 .filename(&self.branch.db_path)
125 .create_if_missing(false)
126 .journal_mode(SqliteJournalMode::Wal),
127 )
128 .await
129 .map_err(crate::error::BranchError::Database)
130 }
131}