vv_agent/runtime/backends/
recipe.rs1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use super::distributed::DistributedCapabilities;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub struct RuntimeRecipe {
8 pub settings_file: String,
9 pub backend: String,
10 pub model: String,
11 pub workspace: String,
12 pub timeout_seconds: f64,
13 pub log_preview_chars: Option<usize>,
14 pub capabilities: DistributedCapabilities,
15}
16
17impl RuntimeRecipe {
18 pub fn new(
19 settings_file: impl Into<String>,
20 backend: impl Into<String>,
21 model: impl Into<String>,
22 workspace: impl Into<String>,
23 ) -> Self {
24 Self {
25 settings_file: settings_file.into(),
26 backend: backend.into(),
27 model: model.into(),
28 workspace: workspace.into(),
29 timeout_seconds: 90.0,
30 log_preview_chars: None,
31 capabilities: DistributedCapabilities::default(),
32 }
33 }
34
35 pub fn to_dict(&self) -> Value {
36 serde_json::json!({
37 "settings_file": self.settings_file,
38 "backend": self.backend,
39 "model": self.model,
40 "workspace": self.workspace,
41 "timeout_seconds": self.timeout_seconds,
42 "log_preview_chars": self.log_preview_chars,
43 "capabilities": self.capabilities.to_dict(),
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 capabilities: DistributedCapabilities::from_dict(
66 object
67 .get("capabilities")
68 .ok_or_else(|| "capabilities must be an object".to_string())?,
69 )?,
70 })
71 }
72
73 pub fn validate(&self) -> Result<(), String> {
74 for (field_name, value) in [
75 ("settings_file", self.settings_file.as_str()),
76 ("backend", self.backend.as_str()),
77 ("model", self.model.as_str()),
78 ("workspace", self.workspace.as_str()),
79 ] {
80 if value.trim().is_empty() {
81 return Err(format!(
82 "runtime_recipe.{field_name} must be a non-empty string"
83 ));
84 }
85 }
86 if !self.timeout_seconds.is_finite() || self.timeout_seconds <= 0.0 {
87 return Err(
88 "runtime_recipe.timeout_seconds must be a finite positive number".to_string(),
89 );
90 }
91 self.capabilities.validate()
92 }
93}
94
95fn read_required_string<'a>(
96 object: &'a serde_json::Map<String, Value>,
97 key: &str,
98) -> Result<&'a str, String> {
99 object
100 .get(key)
101 .and_then(Value::as_str)
102 .ok_or_else(|| format!("missing required string field {key:?}"))
103}