Skip to main content

vv_agent/runtime/backends/
recipe.rs

1use std::io;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::runtime::stores::sqlite::SqliteStateStore;
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct RuntimeRecipe {
11    pub settings_file: String,
12    pub backend: String,
13    pub model: String,
14    pub workspace: String,
15    pub timeout_seconds: f64,
16    pub log_preview_chars: Option<usize>,
17}
18
19impl RuntimeRecipe {
20    pub fn new(
21        settings_file: impl Into<String>,
22        backend: impl Into<String>,
23        model: impl Into<String>,
24        workspace: impl Into<String>,
25    ) -> Self {
26        Self {
27            settings_file: settings_file.into(),
28            backend: backend.into(),
29            model: model.into(),
30            workspace: workspace.into(),
31            timeout_seconds: 90.0,
32            log_preview_chars: None,
33        }
34    }
35
36    pub fn to_dict(&self) -> Value {
37        serde_json::json!({
38            "settings_file": self.settings_file,
39            "backend": self.backend,
40            "model": self.model,
41            "workspace": self.workspace,
42            "timeout_seconds": self.timeout_seconds,
43            "log_preview_chars": self.log_preview_chars,
44        })
45    }
46
47    pub fn from_dict(data: &Value) -> Result<Self, String> {
48        let object = data
49            .as_object()
50            .ok_or_else(|| "RuntimeRecipe payload must be an object".to_string())?;
51        Ok(Self {
52            settings_file: read_required_string(object, "settings_file")?.to_string(),
53            backend: read_required_string(object, "backend")?.to_string(),
54            model: read_required_string(object, "model")?.to_string(),
55            workspace: read_required_string(object, "workspace")?.to_string(),
56            timeout_seconds: object
57                .get("timeout_seconds")
58                .and_then(Value::as_f64)
59                .unwrap_or(90.0),
60            log_preview_chars: object
61                .get("log_preview_chars")
62                .filter(|value| !value.is_null())
63                .and_then(Value::as_u64)
64                .and_then(|value| usize::try_from(value).ok()),
65        })
66    }
67
68    pub fn default_sqlite_checkpoint_path(&self) -> PathBuf {
69        PathBuf::from(&self.workspace)
70            .join(".vv-agent-state")
71            .join("checkpoints.db")
72    }
73
74    pub fn build_default_state_store(&self) -> io::Result<SqliteStateStore> {
75        let db_path = self.default_sqlite_checkpoint_path();
76        if let Some(parent) = db_path.parent() {
77            std::fs::create_dir_all(parent)?;
78        }
79        SqliteStateStore::new(db_path)
80    }
81}
82
83fn read_required_string<'a>(
84    object: &'a serde_json::Map<String, Value>,
85    key: &str,
86) -> Result<&'a str, String> {
87    object
88        .get(key)
89        .and_then(Value::as_str)
90        .ok_or_else(|| format!("missing required string field {key:?}"))
91}