use std::{
path::{Path, PathBuf},
time::Duration,
};
use agent_runtime_authoring::{ScaffoldRequest, TemplateKind};
use agent_runtime_bootstrap::RuntimeBootstrap;
use agent_runtime_config::RuntimeConfig;
use anyhow::Context;
use clap::{Parser, Subcommand, ValueEnum};
#[derive(Parser)]
#[command(name = "agent-runtime")]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[arg(long, env = "RUNTIME_CONFIG_FILE", global = true)]
config: Option<PathBuf>,
}
#[derive(Subcommand)]
enum Command {
Serve,
New {
#[arg(value_enum)]
kind: NewKind,
name: String,
destination: PathBuf,
#[arg(long)]
runtime_path: Option<PathBuf>,
},
Inspect { definition: PathBuf },
Test { definition: PathBuf },
Build {
definition: PathBuf,
output: PathBuf,
},
}
#[derive(Clone, Copy, ValueEnum)]
enum NewKind {
Agent,
RustExtension,
RuntimeDistribution,
}
pub async fn run() -> anyhow::Result<()> {
if Path::new(".env").is_file() {
dotenvy::from_path(".env").context("failed to load project .env")?;
}
let cli = Cli::parse();
match cli.command {
Some(Command::New {
kind,
name,
destination,
runtime_path,
}) => {
let files = agent_runtime_authoring::scaffold(ScaffoldRequest {
kind: match kind {
NewKind::Agent => TemplateKind::Agent,
NewKind::RustExtension => TemplateKind::RustExtension,
NewKind::RuntimeDistribution => TemplateKind::RuntimeDistribution,
},
name: &name,
destination: &destination,
runtime_path: runtime_path.as_deref(),
})?;
println!(
"generated {} files in {}",
files.len(),
destination.display()
);
Ok(())
}
Some(Command::Inspect { definition }) => {
println!(
"{}",
serde_json::to_string_pretty(&agent_runtime_authoring::inspect(&definition)?)?
);
Ok(())
}
Some(Command::Test { definition }) => {
println!(
"{}",
serde_json::to_string_pretty(&agent_runtime_authoring::test_definition(
&definition
)?)?
);
Ok(())
}
Some(Command::Build { definition, output }) => {
let inspection = agent_runtime_authoring::build_artifact(&definition, &output)?;
println!("{} {}", inspection.digest, output.display());
Ok(())
}
Some(Command::Serve) | None => serve(cli.config).await,
}
}
async fn serve(config_path: Option<PathBuf>) -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "agent_runtime=info,runtime_link=info".into()),
)
.init();
let config = RuntimeConfig::load(config_path.as_deref())?;
let definition = agent_runtime_code_agent::definition()?;
let application = RuntimeBootstrap::new(&config)
.with_definition(definition)
.build()
.await?;
let listener = tokio::net::TcpListener::bind(&config.server.bind)
.await
.with_context(|| format!("failed to bind {}", config.server.bind))?;
tracing::info!(bind = %config.server.bind, "Agent Runtime is listening");
let shutdown_grace = Duration::from_secs(config.server.shutdown_grace_seconds);
axum::serve(listener, application.router.clone())
.with_graceful_shutdown(async move {
let _ = tokio::signal::ctrl_c().await;
application.begin_shutdown();
let active_executions = application.active_executions();
tracing::info!(
?shutdown_grace,
active_executions,
"Agent Runtime is draining"
);
let drained = application.shutdown(shutdown_grace).await;
if drained {
tracing::info!("Agent Runtime drained all execution tasks");
} else {
tracing::error!("Agent Runtime shutdown timed out after forced cancellation");
}
})
.await?;
Ok(())
}