agent-runtime-cli 0.1.0

Unified CLI for serving, creating, inspecting, testing, and building Agent Runtime definitions
Documentation
//! Agent Runtime process entry point.

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>,
    /// Runtime TOML configuration file.
    #[arg(long, env = "RUNTIME_CONFIG_FILE", global = true)]
    config: Option<PathBuf>,
}

#[derive(Subcommand)]
enum Command {
    /// Start the Runtime HTTP process.
    Serve,
    /// Create an Agent, Rust Tool extension, or Runtime distribution.
    New {
        #[arg(value_enum)]
        kind: NewKind,
        name: String,
        destination: PathBuf,
        #[arg(long)]
        runtime_path: Option<PathBuf>,
    },
    /// Validate and describe an Agent definition.
    Inspect { definition: PathBuf },
    /// Run deterministic Agent definition and eval validation.
    Test { definition: PathBuf },
    /// Build a deterministic Agent definition artifact.
    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())?;
    // The official CLI selects the reference Code Agent explicitly. Bootstrap
    // itself has no Agent behavior dependency, so a custom distribution can
    // inject a Tool/Skill-only Definition without pulling in Code Agent.
    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(())
}