1pub(crate) mod agent;
2pub(crate) mod agent_key;
3pub(crate) mod agent_runtime;
4pub(crate) mod config_setting;
5pub(crate) mod error;
6pub(crate) mod fake_prompt_mcp;
7pub(crate) mod model_config;
8pub(crate) mod prompt_history_index;
9pub(crate) mod protocol;
10pub(crate) mod session_actor;
11pub(crate) mod session_config_state;
12pub(crate) mod session_factory;
13pub(crate) mod session_store;
14pub(crate) mod slash_commands;
15pub(crate) mod state;
16pub(crate) mod stdio;
17pub mod testing;
18
19pub use protocol::map_mcp_prompt_to_available_command;
20
21use crate::acp::agent::acp_agent_builder;
22use crate::acp::state::{AcpState, AcpStateConfig};
23use crate::acp::stdio::Stdio;
24use crate::provider_connection_args::ProviderConnectionArgs;
25use crate::settings_args::SettingsSourceArgs;
26use agent_client_protocol as acp;
27use llm::ReasoningEffort;
28use std::env::current_dir;
29use std::sync::Arc;
30use std::{fs::create_dir_all, path::PathBuf};
31use thiserror::Error;
32use tracing::info;
33use tracing_appender::rolling::daily;
34use tracing_subscriber::EnvFilter;
35
36use crate::credentials::build_oauth_credential_store;
37use aether_auth::OAuthError;
38use session_factory::InitialSessionSelection;
39use session_store::SessionStore;
40
41#[derive(clap::Args, Debug)]
42pub struct AcpArgs {
43 #[clap(long, default_value = "/tmp/aether-acp-logs")]
45 pub log_dir: PathBuf,
46
47 #[clap(long, conflicts_with_all = ["model", "reasoning_effort"])]
49 pub agent: Option<String>,
50
51 #[clap(long, conflicts_with = "agent")]
54 pub model: Option<String>,
55
56 #[clap(long, value_name = "low|medium|high|xhigh", requires = "model", conflicts_with = "agent")]
58 pub reasoning_effort: Option<ReasoningEffort>,
59
60 #[command(flatten)]
61 pub provider_connection: ProviderConnectionArgs,
62
63 #[command(flatten)]
64 pub settings_source: SettingsSourceArgs,
65}
66
67#[derive(Debug)]
69pub enum AcpRunOutcome {
70 CleanDisconnect,
72}
73
74#[derive(Debug, Error)]
76pub enum AcpRunError {
77 #[error("ACP protocol error: {0}")]
78 Protocol(#[from] acp::Error),
79
80 #[error("Failed to initialize OAuth credential store: {0}")]
81 CredentialStore(#[from] OAuthError),
82}
83
84pub async fn run_acp(args: AcpArgs) -> Result<AcpRunOutcome, AcpRunError> {
85 info!("Starting Aether ACP server");
86
87 setup_logging(&args);
88
89 let initial_selection = if let Some(agent) = args.agent.clone() {
90 InitialSessionSelection::agent(agent)
91 } else if let Some(model) = args.model.clone() {
92 InitialSessionSelection::model(model, args.reasoning_effort)
93 } else {
94 InitialSessionSelection::default()
95 };
96 let session_store =
97 SessionStore::new().map_or_else(|e| panic!("Failed to initialize session store: {e}"), Arc::new);
98 let cwd = current_dir().unwrap_or_else(|_| PathBuf::from("."));
99 let oauth_credential_store = build_oauth_credential_store(&args.settings_source, &cwd)?;
100 let provider_connections = args.provider_connection.into_overrides();
101 let state = Arc::new(AcpState::new(AcpStateConfig {
102 session_store,
103 oauth_credential_store,
104 initial_selection,
105 settings_source: args.settings_source,
106 provider_connections,
107 }));
108
109 let connect_result = acp_agent_builder(state.clone()).connect_to(Stdio::new()).await;
110 state.shutdown_all().await;
111
112 match connect_result {
113 Ok(()) => Ok(AcpRunOutcome::CleanDisconnect),
114 Err(err) => Err(AcpRunError::Protocol(err)),
115 }
116}
117
118fn setup_logging(args: &AcpArgs) {
119 create_dir_all(&args.log_dir).ok();
120 tracing_subscriber::fmt()
121 .with_writer(daily(&args.log_dir, "aether-acp.log"))
122 .with_ansi(false) .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")))
124 .pretty()
125 .init();
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use clap::Parser;
132
133 #[derive(Debug, Parser)]
134 struct TestCli {
135 #[command(flatten)]
136 args: AcpArgs,
137 }
138
139 #[test]
140 fn agent_conflicts_with_model() {
141 let err = TestCli::try_parse_from(["test", "--agent", "planner", "--model", "anthropic:claude-sonnet-4-5"])
142 .expect_err("agent and model should conflict");
143 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
144 }
145
146 #[test]
147 fn agent_conflicts_with_reasoning_effort() {
148 let err = TestCli::try_parse_from(["test", "--agent", "planner", "--reasoning-effort", "high"])
149 .expect_err("agent and reasoning effort should conflict");
150 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
151 }
152
153 #[test]
154 fn reasoning_effort_requires_model() {
155 let err = TestCli::try_parse_from(["test", "--reasoning-effort", "high"])
156 .expect_err("reasoning effort should require model");
157 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
158 }
159
160 #[test]
161 fn reasoning_effort_with_model_is_allowed() {
162 let cli =
163 TestCli::try_parse_from(["test", "--model", "anthropic:claude-sonnet-4-5", "--reasoning-effort", "high"])
164 .expect("reasoning effort can configure an explicit model session");
165 assert_eq!(cli.args.reasoning_effort, Some(ReasoningEffort::High));
166 }
167}