vv_agent/runtime/backends/
recipe.rs1use 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}