Skip to main content

agent_runtime_cli/
lib.rs

1//! Agent Runtime process entry point.
2
3use std::{
4    path::{Path, PathBuf},
5    time::Duration,
6};
7
8use agent_runtime_authoring::{ScaffoldRequest, TemplateKind};
9use agent_runtime_bootstrap::RuntimeBootstrap;
10use agent_runtime_config::RuntimeConfig;
11use anyhow::Context;
12use clap::{Parser, Subcommand, ValueEnum};
13
14#[derive(Parser)]
15#[command(name = "agent-runtime")]
16struct Cli {
17    #[command(subcommand)]
18    command: Option<Command>,
19    /// Runtime TOML configuration file.
20    #[arg(long, env = "RUNTIME_CONFIG_FILE", global = true)]
21    config: Option<PathBuf>,
22}
23
24#[derive(Subcommand)]
25enum Command {
26    /// Start the Runtime HTTP process.
27    Serve,
28    /// Create an Agent, Rust Tool extension, or Runtime distribution.
29    New {
30        #[arg(value_enum)]
31        kind: NewKind,
32        name: String,
33        destination: PathBuf,
34        #[arg(long)]
35        runtime_path: Option<PathBuf>,
36    },
37    /// Validate and describe an Agent definition.
38    Inspect { definition: PathBuf },
39    /// Run deterministic Agent definition and eval validation.
40    Test { definition: PathBuf },
41    /// Build a deterministic Agent definition artifact.
42    Build {
43        definition: PathBuf,
44        output: PathBuf,
45    },
46}
47
48#[derive(Clone, Copy, ValueEnum)]
49enum NewKind {
50    Agent,
51    RustExtension,
52    RuntimeDistribution,
53}
54
55pub async fn run() -> anyhow::Result<()> {
56    if Path::new(".env").is_file() {
57        dotenvy::from_path(".env").context("failed to load project .env")?;
58    }
59    let cli = Cli::parse();
60    match cli.command {
61        Some(Command::New {
62            kind,
63            name,
64            destination,
65            runtime_path,
66        }) => {
67            let files = agent_runtime_authoring::scaffold(ScaffoldRequest {
68                kind: match kind {
69                    NewKind::Agent => TemplateKind::Agent,
70                    NewKind::RustExtension => TemplateKind::RustExtension,
71                    NewKind::RuntimeDistribution => TemplateKind::RuntimeDistribution,
72                },
73                name: &name,
74                destination: &destination,
75                runtime_path: runtime_path.as_deref(),
76            })?;
77            println!(
78                "generated {} files in {}",
79                files.len(),
80                destination.display()
81            );
82            Ok(())
83        }
84        Some(Command::Inspect { definition }) => {
85            println!(
86                "{}",
87                serde_json::to_string_pretty(&agent_runtime_authoring::inspect(&definition)?)?
88            );
89            Ok(())
90        }
91        Some(Command::Test { definition }) => {
92            println!(
93                "{}",
94                serde_json::to_string_pretty(&agent_runtime_authoring::test_definition(
95                    &definition
96                )?)?
97            );
98            Ok(())
99        }
100        Some(Command::Build { definition, output }) => {
101            let inspection = agent_runtime_authoring::build_artifact(&definition, &output)?;
102            println!("{} {}", inspection.digest, output.display());
103            Ok(())
104        }
105        Some(Command::Serve) | None => serve(cli.config).await,
106    }
107}
108
109async fn serve(config_path: Option<PathBuf>) -> anyhow::Result<()> {
110    tracing_subscriber::fmt()
111        .with_env_filter(
112            tracing_subscriber::EnvFilter::try_from_default_env()
113                .unwrap_or_else(|_| "agent_runtime=info,runtime_link=info".into()),
114        )
115        .init();
116    let config = RuntimeConfig::load(config_path.as_deref())?;
117    // The official CLI selects the reference Code Agent explicitly. Bootstrap
118    // itself has no Agent behavior dependency, so a custom distribution can
119    // inject a Tool/Skill-only Definition without pulling in Code Agent.
120    let definition = agent_runtime_code_agent::definition()?;
121    let application = RuntimeBootstrap::new(&config)
122        .with_definition(definition)
123        .build()
124        .await?;
125    let listener = tokio::net::TcpListener::bind(&config.server.bind)
126        .await
127        .with_context(|| format!("failed to bind {}", config.server.bind))?;
128    tracing::info!(bind = %config.server.bind, "Agent Runtime is listening");
129    let shutdown_grace = Duration::from_secs(config.server.shutdown_grace_seconds);
130    axum::serve(listener, application.router.clone())
131        .with_graceful_shutdown(async move {
132            let _ = tokio::signal::ctrl_c().await;
133            application.begin_shutdown();
134            let active_executions = application.active_executions();
135            tracing::info!(
136                ?shutdown_grace,
137                active_executions,
138                "Agent Runtime is draining"
139            );
140            let drained = application.shutdown(shutdown_grace).await;
141            if drained {
142                tracing::info!("Agent Runtime drained all execution tasks");
143            } else {
144                tracing::error!("Agent Runtime shutdown timed out after forced cancellation");
145            }
146        })
147        .await?;
148    Ok(())
149}