use jamjet_agents::{InMemoryAgentRegistry, SqliteAgentRegistry};
use jamjet_api::{config::ApiConfig, routes::build_router_with_opts, state::AppState};
use jamjet_audit::{AuditEnricher, NoopAuditBackend, SqliteAuditBackend};
use jamjet_state::{InMemoryBackend, SqliteBackend};
use std::sync::Arc;
use tracing::info;
fn parse_store_term(raw: &str) -> anyhow::Result<i64> {
let trimmed = raw.trim();
let term = trimmed
.parse::<i64>()
.map_err(|e| anyhow::anyhow!("invalid JAMJET_STORE_TERM `{trimmed}`: {e}"))?;
anyhow::ensure!(
term >= 0,
"JAMJET_STORE_TERM must be non-negative, got {term}"
);
Ok(term)
}
fn store_term_from_env() -> anyhow::Result<Option<i64>> {
match std::env::var("JAMJET_STORE_TERM") {
Ok(raw) => parse_store_term(&raw).map(Some),
Err(std::env::VarError::NotPresent) => Ok(None),
Err(e) => anyhow::bail!("failed to read JAMJET_STORE_TERM: {e}"),
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let config = ApiConfig::default();
let otel_endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
jamjet_telemetry::init(config.dev_mode, otel_endpoint.as_deref());
let storage_backend = std::env::var("STORAGE_BACKEND").unwrap_or_default();
let configured_store_term = store_term_from_env()?;
let storage_label = if storage_backend == "memory" {
"memory"
} else {
"sqlite"
};
info!(
port = config.port,
dev_mode = config.dev_mode,
storage = %storage_label,
"Starting JamJet runtime"
);
let state = if storage_backend == "memory" {
info!("Using in-memory storage (ephemeral, no persistence)");
let backend = Arc::new(InMemoryBackend::new());
if let Some(term) = configured_store_term {
let applied = backend.set_store_term_at_least(term);
info!(
store_term = applied,
"Applied failover generation from JAMJET_STORE_TERM"
);
}
let backend_clone = backend.clone();
let audit: Arc<dyn jamjet_audit::AuditBackend> = Arc::new(NoopAuditBackend);
let enricher = Arc::new(AuditEnricher::new(Arc::clone(&audit)));
AppState {
backend: backend.clone() as Arc<dyn jamjet_state::StateBackend>,
backend_for_fn: Arc::new(move |_tenant_id: &jamjet_state::TenantId| {
backend_clone.clone() as Arc<dyn jamjet_state::StateBackend>
}),
agents: Arc::new(InMemoryAgentRegistry::new()),
audit,
enricher,
protocols: jamjet_api::state::default_protocol_registry(),
cron_store: None,
}
} else {
let database_url = std::env::var("DATABASE_URL")
.ok()
.or_else(|| config.database_url.clone())
.unwrap_or_else(|| {
let dir = std::path::PathBuf::from(".jamjet");
std::fs::create_dir_all(&dir).ok();
format!("sqlite://{}", dir.join("runtime.db").display())
});
info!(%database_url, "Connecting to state backend");
let backend = SqliteBackend::open(&database_url)
.await
.map_err(|e| anyhow::anyhow!("failed to open state backend: {e}"))?;
if let Some(term) = configured_store_term {
let applied = backend
.set_store_term_at_least(term)
.await
.map_err(|e| anyhow::anyhow!("failed to set store term: {e}"))?;
info!(
store_term = applied,
"Applied failover generation from JAMJET_STORE_TERM"
);
}
let agents = SqliteAgentRegistry::connect(&database_url)
.await
.map_err(|e| anyhow::anyhow!("failed to open agent registry: {e}"))?;
let audit_backend = SqliteAuditBackend::open(&database_url)
.await
.map_err(|e| anyhow::anyhow!("failed to open audit backend: {e}"))?;
audit_backend
.migrate()
.await
.map_err(|e| anyhow::anyhow!("failed to migrate audit log: {e}"))?;
let audit: Arc<dyn jamjet_audit::AuditBackend> = Arc::new(audit_backend);
let enricher = Arc::new(AuditEnricher::new(Arc::clone(&audit)));
let sqlite = Arc::new(backend);
let sqlite_clone = sqlite.clone();
AppState {
backend: sqlite.clone() as Arc<dyn jamjet_state::StateBackend>,
backend_for_fn: Arc::new(move |tenant_id: &jamjet_state::TenantId| {
Arc::new(sqlite_clone.for_tenant(tenant_id.clone()))
as Arc<dyn jamjet_state::StateBackend>
}),
agents: Arc::new(agents),
audit,
enricher,
protocols: jamjet_api::state::default_protocol_registry(),
cron_store: Some(Arc::new(jamjet_timers::CronStore::new(sqlite.pool()))),
}
};
let scheduler = jamjet_scheduler::Scheduler::new(state.backend.clone());
tokio::spawn(async move { scheduler.run().await });
info!("Scheduler started");
let projector = jamjet_scheduler::Projector::new(state.backend.clone());
tokio::spawn(async move { projector.run().await });
info!("Projector started");
if config.dev_mode || std::env::var("JAMJET_EMBED_WORKERS").is_ok() {
let model_registry = Arc::new(
jamjet_models::registry::registry_from_env_checked()
.await
.map_err(|e| anyhow::anyhow!("model seam coverage guard failed: {e}"))?,
);
let pool = jamjet_worker::default_pool(state.backend.clone())
.with_executor(
"model",
Arc::new(jamjet_worker::ModelNodeExecutor::new(
model_registry.clone(),
)),
)
.with_executor(
"eval",
Arc::new(jamjet_worker::EvalExecutor::new(model_registry.clone())),
)
.with_executor(
"condition",
Arc::new(jamjet_worker::ConditionNodeExecutor::new()),
);
let _worker_handles = pool.spawn();
info!("Embedded worker pool started (dev mode)");
}
if (config.dev_mode || std::env::var("JAMJET_EMBED_CRON").is_ok())
&& storage_backend != "memory"
{
let host = if config.bind == "0.0.0.0" {
"127.0.0.1".to_string()
} else {
config.bind.clone()
};
let self_url = format!("http://{}:{}", host, config.port);
let database_url = std::env::var("DATABASE_URL")
.ok()
.or_else(|| config.database_url.clone())
.unwrap_or_else(|| {
format!(
"sqlite://{}",
std::path::PathBuf::from(".jamjet")
.join("runtime.db")
.display()
)
});
match jamjet_state::SqliteBackend::open(&database_url).await {
Ok(b) => {
let cron = jamjet_timers::CronScheduler::new(b.pool(), self_url);
tokio::spawn(cron.run());
info!("Cron scheduler started (dev mode)");
}
Err(e) => tracing::warn!(error = %e, "cron scheduler: failed to open pool; skipping"),
}
}
let router = build_router_with_opts(state, config.dev_mode);
let addr = format!("{}:{}", config.bind, config.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
info!("JamJet runtime listening on {addr}");
axum::serve(listener, router)
.with_graceful_shutdown(shutdown_signal())
.await?;
info!("Shutting down");
Ok(())
}
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl-C handler");
}
#[cfg(test)]
mod tests {
use super::parse_store_term;
#[test]
fn parses_valid_non_negative() {
assert_eq!(parse_store_term("5").unwrap(), 5);
assert_eq!(parse_store_term(" 0 ").unwrap(), 0);
}
#[test]
fn rejects_invalid_and_negative() {
assert!(parse_store_term("abc").is_err());
assert!(parse_store_term("-1").is_err());
assert!(parse_store_term("").is_err());
}
}