use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LongRunningConfig {
pub endpoint: String,
pub model: String,
pub max_duration: Duration,
pub timeout_per_project_secs: u64,
pub max_iterations: usize,
pub templates_dir: PathBuf,
pub output_dir: PathBuf,
pub max_concurrent: usize,
pub max_tokens: usize,
pub temperature: f32,
pub context_length: usize,
#[serde(default)]
pub extra_body: serde_json::Value,
pub yolo_mode: bool,
pub require_verification: bool,
}
impl Default for LongRunningConfig {
fn default() -> Self {
Self {
endpoint: "http://127.0.0.1:1234/v1".into(),
model: "qwen3.5-27b".into(),
max_duration: Duration::from_secs(8 * 3600), timeout_per_project_secs: 900, max_iterations: 80,
templates_dir: PathBuf::from("system_tests/projecte2e/templates"),
output_dir: PathBuf::from("long_run_results"),
max_concurrent: 1, max_tokens: 16384,
temperature: 0.6,
context_length: 262144,
extra_body: serde_json::json!({
"chat_template_kwargs": {"enable_thinking": false}
}),
yolo_mode: true,
require_verification: true,
}
}
}
impl LongRunningConfig {
pub fn new(endpoint: impl Into<String>, model: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
model: model.into(),
..Default::default()
}
}
pub fn with_duration(mut self, duration: Duration) -> Self {
self.max_duration = duration;
self
}
pub fn with_max_concurrent(mut self, max_concurrent: usize) -> Self {
self.max_concurrent = max_concurrent.max(1);
self
}
pub fn with_project_timeout(mut self, secs: u64) -> Self {
self.timeout_per_project_secs = secs;
self
}
pub fn with_max_iterations(mut self, iters: usize) -> Self {
self.max_iterations = iters;
self
}
pub fn with_templates_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.templates_dir = dir.into();
self
}
pub fn with_output_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.output_dir = dir.into();
self
}
pub fn validate(&self) -> Result<(), String> {
if self.endpoint.is_empty() {
return Err("endpoint must not be empty".into());
}
if self.model.is_empty() {
return Err("model must not be empty".into());
}
if self.timeout_per_project_secs == 0 {
return Err("timeout_per_project_secs must be >= 1".into());
}
if self.max_iterations == 0 {
return Err("max_iterations must be >= 1".into());
}
if !self.templates_dir.exists() {
return Err(format!(
"templates_dir does not exist: {}",
self.templates_dir.display()
));
}
Ok(())
}
pub fn to_selfware_toml(&self) -> String {
format!(
r#"endpoint = "{endpoint}"
model = "{model}"
max_tokens = {max_tokens}
context_length = {context_length}
temperature = {temperature}
[safety]
allowed_paths = ["./**", "/tmp/**"]
[agent]
max_iterations = {max_iterations}
step_timeout_secs = {step_timeout}
native_function_calling = false
streaming = true
min_completion_steps = 5
require_verification_before_completion = {require_verification}
[continuous_work]
enabled = true
checkpoint_interval_tools = 5
auto_recovery = true
max_recovery_attempts = 3
[retry]
max_retries = 3
base_delay_ms = 2000
max_delay_ms = 30000
{extra_body}"#,
endpoint = self.endpoint,
model = self.model,
max_tokens = self.max_tokens,
context_length = self.context_length,
temperature = self.temperature,
max_iterations = self.max_iterations,
step_timeout = self.timeout_per_project_secs,
require_verification = self.require_verification,
extra_body = self.extra_body_toml_section(),
)
}
fn extra_body_toml_section(&self) -> String {
let is_empty = self
.extra_body
.as_object()
.map(|o| o.is_empty())
.unwrap_or(true);
if is_empty {
return String::new();
}
let mut wrapper = serde_json::Map::new();
wrapper.insert("extra_body".to_string(), self.extra_body.clone());
toml::to_string(&serde_json::Value::Object(wrapper)).unwrap_or_default()
}
}
#[cfg(test)]
#[path = "../../../tests/unit/bench_harness/long_running/config/config_test.rs"]
mod tests;