use std::process::ExitCode;
use std::sync::Arc;
use agentplane::journal::JournalStore;
use agentplane::manifest::Manifest;
use agentplane::model::ModelProvider;
use agentplane::runtime::{Mode, RunStatus, Runtime, RuntimeBuilder};
use agentplane::store::RedbStore;
const USAGE: &str = "\
agentplane — run an agent that is only a file
USAGE
agentplane run <manifest.yaml> [OPTIONS]
agentplane validate <manifest.yaml>
agentplane digest <manifest.yaml>
OPTIONS
--input <JSON> the run's input; defaults to {}
--input-file <PATH> read the input from a file instead
--store <PATH> journal on disk; defaults to memory, which keeps nothing
--replay <RUN_ID> re-execute a recorded run instead of starting one
--strict with --replay: verify rather than resume
PROVIDERS
The manifest names a provider; the key comes from the environment, never
from the file — an agent's declaration must not change when its key does.
anthropic ANTHROPIC_API_KEY
bedrock AWS_REGION plus the standard AWS credential chain
openai OPENAI_API_KEY
fake no key. Answers deterministically without a network, so a
manifest can be exercised before anyone pays for it.
";
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(&args) {
Ok(code) => code,
Err(e) => {
eprintln!("agentplane: {e}");
ExitCode::FAILURE
}
}
}
fn run(args: &[String]) -> Result<ExitCode, String> {
let Some(verb) = args.first().map(String::as_str) else {
print!("{USAGE}");
return Ok(ExitCode::FAILURE);
};
if matches!(verb, "-h" | "--help" | "help") {
print!("{USAGE}");
return Ok(ExitCode::SUCCESS);
}
let path = args
.get(1)
.ok_or_else(|| format!("`{verb}` needs a manifest path. See --help"))?;
let text = std::fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?;
let manifest = Manifest::parse(&text).map_err(|e| e.to_string())?;
match verb {
"validate" => {
println!(
"ok: {} {}",
manifest.metadata.name, manifest.metadata.version
);
Ok(ExitCode::SUCCESS)
}
"digest" => {
println!("{}", manifest.digest().map_err(|e| e.to_string())?.to_hex());
Ok(ExitCode::SUCCESS)
}
"run" => execute(&manifest, &Options::parse(&args[2..])?),
other => Err(format!("unknown command `{other}`. See --help")),
}
}
#[derive(Default)]
struct Options {
input: Option<String>,
input_file: Option<String>,
store: Option<String>,
replay: Option<String>,
strict: bool,
}
impl Options {
fn parse(args: &[String]) -> Result<Self, String> {
let mut o = Self::default();
let mut it = args.iter();
while let Some(flag) = it.next() {
let mut value = || {
it.next()
.cloned()
.ok_or_else(|| format!("{flag} needs a value"))
};
match flag.as_str() {
"--input" => o.input = Some(value()?),
"--input-file" => o.input_file = Some(value()?),
"--store" => o.store = Some(value()?),
"--replay" => o.replay = Some(value()?),
"--strict" => o.strict = true,
other => return Err(format!("unknown option `{other}`. See --help")),
}
}
if o.input.is_some() && o.input_file.is_some() {
return Err("--input and --input-file both given; which one did you mean?".into());
}
Ok(o)
}
fn read_input(&self) -> Result<serde_json::Value, String> {
let text = match (&self.input, &self.input_file) {
(Some(s), _) => s.clone(),
(_, Some(p)) => std::fs::read_to_string(p).map_err(|e| format!("reading {p}: {e}"))?,
_ => "{}".into(),
};
serde_json::from_str(&text).map_err(|e| format!("the input is not valid JSON: {e}"))
}
}
fn execute(manifest: &Manifest, opts: &Options) -> Result<ExitCode, String> {
if manifest.spec.execution.is_none() {
return Err(format!(
"manifest '{}' declares no `spec.execution`, so its behaviour is a skill somebody \
wrote and there is nothing here for this binary to run. Register it in your own \
binary with `RuntimeBuilder::agent(Agent::new(&manifest).skill(YourSkill))` instead",
manifest.metadata.name
));
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("could not start the async runtime: {e}"))?;
rt.block_on(async {
let store: Arc<dyn JournalStore> = if let Some(path) = &opts.store {
Arc::new(RedbStore::open(path).map_err(|e| e.to_string())?)
} else {
eprintln!("note: journaling to memory; this run will not survive the process");
Arc::new(RedbStore::open_in_memory().map_err(|e| e.to_string())?)
};
let builder = with_providers(Runtime::builder(Arc::clone(&store)), manifest).await?;
let agent = builder
.agent(agentplane::runtime::Agent::new(manifest))
.build();
let outcome = if let Some(id) = &opts.replay {
let run = agentplane::core::RunId::parse(id)
.map_err(|e| format!("`{id}` is not a run id: {e}"))?;
let mode = if opts.strict {
Mode::Strict
} else {
Mode::Resume
};
agent.replay(run, mode).await
} else {
let capability = manifest
.spec
.capabilities
.provides
.first()
.ok_or("the manifest provides no capability to run")?;
agent.run(capability, opts.read_input()?).await
}
.map_err(|e| e.to_string())?;
eprintln!("run {} — {:?}", outcome.run_id, outcome.status);
if let Some(output) = &outcome.output {
println!("{output}");
}
Ok(if matches!(outcome.status, RunStatus::Succeeded) {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
})
}
async fn with_providers(
builder: RuntimeBuilder,
manifest: &Manifest,
) -> Result<RuntimeBuilder, String> {
let Some(models) = &manifest.spec.models else {
return Ok(builder);
};
let mut builder = builder;
let mut seen: Vec<String> = Vec::new();
for m in [models.privileged.as_ref(), models.quarantined.as_ref()]
.into_iter()
.flatten()
{
if seen.contains(&m.provider) {
continue;
}
seen.push(m.provider.clone());
builder = builder.provider(m.provider.clone(), driver(&m.provider).await?);
}
Ok(builder)
}
async fn driver(name: &str) -> Result<Arc<dyn ModelProvider>, String> {
match name {
#[cfg(feature = "providers")]
"anthropic" => Ok(Arc::new(
agentplane::model::anthropic::Anthropic::new(key("ANTHROPIC_API_KEY")?)
.map_err(|e| e.to_string())?,
)),
#[cfg(feature = "bedrock")]
"bedrock" => Ok(Arc::new(
agentplane::model::bedrock::Bedrock::from_env(
std::env::var("AWS_REGION").map_err(|_| {
"AWS_REGION is not set, and the manifest names Bedrock".to_owned()
})?,
)
.await?,
)),
#[cfg(feature = "providers")]
"openai" => Ok(Arc::new(
agentplane::model::openai::OpenAi::new(key("OPENAI_API_KEY")?)
.map_err(|e| e.to_string())?,
)),
#[cfg(feature = "testkit")]
"fake" => Ok(agentplane::testkit::FakeProvider::new()),
other => Err(format!(
"no driver for provider '{other}'. This binary ships anthropic, bedrock, openai and fake; \
anything else is an embedder's own driver, registered through \
RuntimeBuilder::provider"
)),
}
}
#[cfg(feature = "providers")]
fn key(var: &str) -> Result<String, String> {
std::env::var(var)
.map_err(|_| format!("{var} is not set, and the manifest names a provider that needs it"))
}