Files
ForgeLM/tauri-app/src-tauri/src/runtime/mod.rs
T
Evandro Fonseca Junior 77c57c3d45 Add runtime state validation before ForgeAgent HTTP call
- Check process_running and ready state before making inference request
- Emit descriptive error messages for each failure scenario
- Handle local vs remote connection modes appropriately
- Validation score: 100 (was 0)

Result: {"status":"keep","validation_score":100,"response_time_ms":500}
2026-04-02 14:37:01 -03:00

309 lines
10 KiB
Rust

pub mod config;
pub mod health;
pub mod process;
pub mod state;
pub mod types;
use std::time::Duration;
use tauri::{AppHandle, State};
use self::config::{load_runtime_config, save_runtime_config};
use self::health::wait_for_health;
use self::process::spawn_localllm;
use self::state::{
begin_lifecycle_command, block_lifecycle_command, build_snapshot, finish_lifecycle_command,
guard_start_request, guard_stop_request, refresh_process_state, requires_runtime_restart,
runtime_error,
};
use self::types::{
ConnectionMode, RuntimeConfig, RuntimeLifecycleCommand, RuntimeLifecycleOutcome,
RuntimeSnapshot, RuntimeStatus, StartPolicy,
};
pub fn get_runtime_snapshot(state: State<'_, RuntimeState>) -> Result<RuntimeSnapshot, String> {
let mut inner = state.0.lock().map_err(|e| e.to_string())?;
refresh_process_state(&mut inner);
Ok(build_snapshot(&inner))
}
pub fn save_runtime_config_command(
app: AppHandle,
state: State<'_, RuntimeState>,
config: RuntimeConfig,
) -> Result<RuntimeSnapshot, String> {
let mut inner = state.0.lock().map_err(|e| e.to_string())?;
refresh_process_state(&mut inner);
if inner.child.is_some() && requires_runtime_restart(&inner.config, &config) {
let message =
"Stop the local API before changing connection mode, model, address, or runtime sizing.";
inner.last_error = Some(runtime_error("restart_required", message));
block_lifecycle_command(&mut inner, RuntimeLifecycleCommand::Save, message);
return Ok(build_snapshot(&inner));
}
begin_lifecycle_command(&mut inner, RuntimeLifecycleCommand::Save);
if let Err(error) = save_runtime_config(&app, &config) {
inner.last_error = Some(runtime_error("save_failed", &error));
finish_lifecycle_command(
&mut inner,
RuntimeLifecycleCommand::Save,
RuntimeLifecycleOutcome::Failed,
format!("Failed to save runtime settings: {error}"),
);
return Err(error);
}
inner.config = config;
inner.last_error = None;
finish_lifecycle_command(
&mut inner,
RuntimeLifecycleCommand::Save,
RuntimeLifecycleOutcome::Succeeded,
"Runtime settings saved.",
);
Ok(build_snapshot(&inner))
}
fn start_runtime_inner(
app: &AppHandle,
state: &State<'_, RuntimeState>,
override_config: Option<RuntimeConfig>,
startup_status: RuntimeStatus,
) -> Result<RuntimeSnapshot, String> {
let command = if override_config.is_some() {
RuntimeLifecycleCommand::SaveAndStart
} else {
RuntimeLifecycleCommand::Start
};
let config = {
let mut inner = state.0.lock().map_err(|e| e.to_string())?;
refresh_process_state(&mut inner);
if guard_start_request(&mut inner).is_err() {
return Ok(build_snapshot(&inner));
}
begin_lifecycle_command(&mut inner, command);
if let Some(config) = override_config {
if let Err(error) = save_runtime_config(app, &config) {
inner.last_error = Some(runtime_error("save_failed", &error));
finish_lifecycle_command(
&mut inner,
command,
RuntimeLifecycleOutcome::Failed,
format!("Failed to save runtime settings: {error}"),
);
return Err(error);
}
inner.config = config;
}
if inner.config.connection_mode != ConnectionMode::Local {
let message = "Switch to Local mode before starting the built-in API";
inner.last_error = Some(runtime_error("remote_mode", message));
inner.status = RuntimeStatus::Error;
finish_lifecycle_command(
&mut inner,
command,
RuntimeLifecycleOutcome::Failed,
message,
);
return Ok(build_snapshot(&inner));
}
inner.status = startup_status;
inner.last_error = None;
inner.config.clone()
};
let child = match spawn_localllm(app, &config) {
Ok(child) => child,
Err(message) => {
let mut inner = state.0.lock().map_err(|e| e.to_string())?;
inner.status = RuntimeStatus::Error;
inner.last_error = Some(runtime_error("spawn_failed", &message));
finish_lifecycle_command(
&mut inner,
command,
RuntimeLifecycleOutcome::Failed,
message,
);
return Ok(build_snapshot(&inner));
}
};
{
let mut inner = state.0.lock().map_err(|e| e.to_string())?;
inner.child = Some(child);
inner.status = startup_status;
}
if wait_for_health(&config.addr, Duration::from_secs(15)) {
let mut inner = state.0.lock().map_err(|e| e.to_string())?;
inner.status = RuntimeStatus::Ready;
inner.last_error = None;
let message = if command == RuntimeLifecycleCommand::SaveAndStart {
"Runtime settings saved and local API started."
} else {
"Local API started."
};
finish_lifecycle_command(
&mut inner,
command,
RuntimeLifecycleOutcome::Succeeded,
message,
);
Ok(build_snapshot(&inner))
} else {
let mut inner = state.0.lock().map_err(|e| e.to_string())?;
if let Some(child) = inner.child.as_mut() {
let _ = child.kill();
let _ = child.wait();
}
inner.child = None;
inner.status = RuntimeStatus::Error;
let message = if startup_status == RuntimeStatus::Loading {
format!(
"Model loading timed out before the local API became ready on {}",
config.addr
)
} else {
format!(
"Local API did not pass /health readiness on {} in time",
config.addr
)
};
inner.last_error = Some(runtime_error("health_timeout", &message));
finish_lifecycle_command(
&mut inner,
command,
RuntimeLifecycleOutcome::Failed,
message,
);
Ok(build_snapshot(&inner))
}
}
pub fn bootstrap_runtime(
app: AppHandle,
state: State<'_, RuntimeState>,
) -> Result<RuntimeSnapshot, String> {
use crate::model_alias;
use crate::model_library::paths::resolve_models_dir;
let mut config = load_runtime_config(&app).unwrap_or_default();
// Auto-resolve model if none is configured yet
let model_path_empty = config
.model_path
.as_deref()
.map(|p| p.trim().is_empty())
.unwrap_or(true);
if model_path_empty && config.connection_mode == ConnectionMode::Local {
let models_dir = resolve_models_dir(config.models_dir.as_deref());
let models = if models_dir.exists() {
let dir_str = models_dir.to_string_lossy().to_string();
crate::model_files::scan_models(dir_str).unwrap_or_default()
} else {
Vec::new()
};
if let Some(resolution) = model_alias::resolve_startup_model(
config.preferred_startup_model_alias.as_deref(),
&models,
) {
eprintln!(
"[bootstrap] auto-selected model: {} ({:?}) reason={:?}",
resolution.alias.as_str(),
resolution.model_path,
resolution.reason
);
config.model_path = Some(resolution.model_path);
// Auto-enable OnAppLaunch when we found a model automatically
config.start_policy = StartPolicy::OnAppLaunch;
}
}
let should_autostart = config.connection_mode == ConnectionMode::Local
&& config.start_policy == StartPolicy::OnAppLaunch
&& config
.model_path
.as_deref()
.map(|value| !value.trim().is_empty())
.unwrap_or(false);
{
let mut inner = state.0.lock().map_err(|e| e.to_string())?;
inner.config = config.clone();
refresh_process_state(&mut inner);
}
if should_autostart {
start_runtime_inner(&app, &state, Some(config), RuntimeStatus::Loading)
} else {
get_runtime_snapshot(state)
}
}
pub fn start_runtime(
app: AppHandle,
state: State<'_, RuntimeState>,
) -> Result<RuntimeSnapshot, String> {
start_runtime_inner(&app, &state, None, RuntimeStatus::Starting)
}
pub fn start_runtime_with_status(
app: AppHandle,
state: State<'_, RuntimeState>,
startup_status: RuntimeStatus,
) -> Result<RuntimeSnapshot, String> {
start_runtime_inner(&app, &state, None, startup_status)
}
pub fn stop_runtime(state: State<'_, RuntimeState>) -> Result<RuntimeSnapshot, String> {
let mut inner = state.0.lock().map_err(|e| e.to_string())?;
refresh_process_state(&mut inner);
if guard_stop_request(&mut inner).is_err() {
return Ok(build_snapshot(&inner));
}
begin_lifecycle_command(&mut inner, RuntimeLifecycleCommand::Stop);
inner.status = RuntimeStatus::Stopping;
if let Some(child) = inner.child.as_mut() {
if let Err(error) = child.kill() {
inner.status = RuntimeStatus::Error;
inner.last_error = Some(runtime_error(
"stop_failed",
format!("Failed to kill server: {error}"),
));
finish_lifecycle_command(
&mut inner,
RuntimeLifecycleCommand::Stop,
RuntimeLifecycleOutcome::Failed,
format!("Failed to stop local API: {error}"),
);
return Ok(build_snapshot(&inner));
}
let _ = child.wait();
}
inner.child = None;
inner.status = RuntimeStatus::Stopped;
inner.last_error = None;
finish_lifecycle_command(
&mut inner,
RuntimeLifecycleCommand::Stop,
RuntimeLifecycleOutcome::Succeeded,
"Local API stopped.",
);
Ok(build_snapshot(&inner))
}
pub use state::RuntimeState;