use std::{net::SocketAddr, path::Path, process::ExitCode};
use basis_a2a::{
A2aServer, A2aService, AgentIdentity, BearerToken, InboundApproval, RunDefaults, ServeConfig,
};
use basis_host::ApprovalPolicy;
use crate::{
cli::{self, A2aArgs, AcpArgs},
exit::{EXIT_FAILED, EXIT_OK},
};
const A2A_RETRY: &str =
"next: retry with `basis serve --a2a <ADDR>` after addressing the reported failure";
pub(crate) async fn serve_a2a(acp: AcpArgs, args: A2aArgs, bind: SocketAddr) -> ExitCode {
let config = match a2a_config(acp, args) {
Ok(config) => config,
Err(message) => return failed(&message),
};
let service = match A2aService::open(config) {
Ok(service) => service,
Err(error) => return failed(&format!("a2a: {error}")),
};
let server = match A2aServer::bind(bind, service).await {
Ok(server) => server,
Err(error) => return failed(&format!("a2a: {error}")),
};
eprintln!("basis: a2a listening on {}", server.endpoint());
eprintln!(
"basis: a2a card at http://{}{}",
server.local_addr(),
basis_a2a::AGENT_CARD_PATH
);
let shutdown = async {
let _ = tokio::signal::ctrl_c().await;
};
match server.serve_until(shutdown).await {
Ok(()) => ExitCode::from(EXIT_OK),
Err(error) => failed(&format!("a2a: {error}")),
}
}
fn failed(message: &str) -> ExitCode {
eprintln!("basis: {message}");
eprintln!("{A2A_RETRY}");
ExitCode::from(EXIT_FAILED)
}
fn a2a_config(acp: AcpArgs, args: A2aArgs) -> Result<ServeConfig, String> {
let approval = match acp.approve {
None | Some(ApprovalPolicy::Never) => InboundApproval::Never,
Some(ApprovalPolicy::Always) => InboundApproval::Always,
Some(ApprovalPolicy::Prompt) => {
return Err(
"--approve prompt cannot serve A2A: the caller is another agent, with nobody \
behind it to answer; use never or always"
.to_string(),
);
}
};
let workspace = match args.workspace {
Some(workspace) => workspace,
None => std::env::current_dir()
.map_err(|error| format!("read the current directory: {error}"))?,
};
let name = args.name.unwrap_or_else(|| directory_name(&workspace));
let description = args
.description
.unwrap_or_else(|| format!("A basis agent working in {name}."));
let mut run = RunDefaults::default();
if let Some(provider) = acp.provider {
run = run.with_provider(provider);
}
if let Some(base_url) = acp.base_url {
run = run.with_base_url(base_url);
}
if let Some(model) = acp.model {
run = run.with_model(model);
}
if let Some(effort) = acp.effort {
run = run.with_effort(effort.into());
}
if let Some(system_prompt) = cli::system_prompt(acp.system_prompt, acp.append_system_prompt) {
run = run.with_system_prompt(system_prompt);
}
if let Some(deadline) = args.deadline {
run = run.with_deadline(deadline.duration());
}
if let Some(tool_budget) = args.tool_budget {
run = run.with_tool_budget(tool_budget);
}
if let Some(token_budget) = args.token_budget {
run = run.with_token_budget(token_budget);
}
let mut config = ServeConfig::new(workspace, AgentIdentity::new(name, description))
.with_approval(approval)
.with_run_defaults(run);
if args.shell {
config = config.with_shell();
}
if let Some(variable) = args.token_env {
config = config.with_token(token_from(&variable)?);
}
if let Some(public_url) = args.public_url {
config = config.with_public_url(public_url);
}
Ok(config)
}
fn token_from(variable: &str) -> Result<BearerToken, String> {
let value = std::env::var(variable)
.map_err(|_| format!("--token-env names {variable}, which is not set"))?;
BearerToken::new(value).ok_or_else(|| format!("{variable} is empty; a token must be set"))
}
fn directory_name(workspace: &Path) -> String {
workspace
.canonicalize()
.ok()
.as_deref()
.unwrap_or(workspace)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.filter(|name| !name.is_empty())
.unwrap_or_else(|| "basis".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prompt_is_refused_before_anything_listens() {
let acp = AcpArgs {
approve: Some(ApprovalPolicy::Prompt),
..AcpArgs::default()
};
let error = a2a_config(acp, A2aArgs::default()).unwrap_err();
assert!(error.contains("nobody"), "{error}");
}
#[test]
fn an_unset_token_variable_is_named_and_its_value_never_is() {
let error = token_from("BASIS_A2A_TEST_TOKEN_THAT_IS_NOT_SET").unwrap_err();
assert!(
error.contains("BASIS_A2A_TEST_TOKEN_THAT_IS_NOT_SET"),
"{error}"
);
}
#[test]
fn the_default_name_is_the_workspace_directorys() {
let dir = tempfile::tempdir().unwrap();
let named = dir.path().join("my-repo");
std::fs::create_dir(&named).unwrap();
assert_eq!(directory_name(&named), "my-repo");
}
}