1use 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 #[arg(long, env = "RUNTIME_CONFIG_FILE", global = true)]
21 config: Option<PathBuf>,
22}
23
24#[derive(Subcommand)]
25enum Command {
26 Serve,
28 New {
30 #[arg(value_enum)]
31 kind: NewKind,
32 name: String,
33 destination: PathBuf,
34 #[arg(long)]
35 runtime_path: Option<PathBuf>,
36 },
37 Inspect { definition: PathBuf },
39 Test { definition: PathBuf },
41 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 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}