use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use anyhow::Result;
use serde_json::{Value, json};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::brain::agent::{AgentService, QueuedUserMessage};
use crate::brain::provider::create_provider_by_name;
use crate::cli::session_resolve::resolve_or_create_session;
use crate::services::{MessageService, SessionService};
use crate::utils::provider_pair::parse_pair;
use super::catalog;
use super::protocol::{self, ClientMessage};
use super::transport::{Transport, TransportHandle};
use super::turn;
pub type SteerMap = Arc<Mutex<HashMap<Uuid, VecDeque<QueuedUserMessage>>>>;
pub fn new_steer_map() -> SteerMap {
Arc::new(Mutex::new(HashMap::new()))
}
pub struct SessionState {
pub id: Uuid,
pub model: Mutex<Option<String>>,
pub mode: Mutex<protocol::AcpMode>,
pub active_cancel: Mutex<Option<CancellationToken>>,
}
pub struct ServerState {
pub handle: TransportHandle,
pub agent: Arc<AgentService>,
pub sessions: SessionService,
pub messages: MessageService,
pub states: Mutex<HashMap<String, Arc<SessionState>>>,
pub steer: SteerMap,
pub default_model: Option<String>,
pub config: Arc<crate::config::Config>,
}
pub struct AcpServer {
state: Arc<ServerState>,
transport: Transport,
}
impl AcpServer {
pub fn new(
agent: Arc<AgentService>,
sessions: SessionService,
messages: MessageService,
default_model: Option<String>,
steer: SteerMap,
config: Arc<crate::config::Config>,
) -> Self {
let transport = Transport::spawn();
let state = Arc::new(ServerState {
handle: transport.handle(),
agent,
sessions,
messages,
states: Mutex::new(HashMap::new()),
steer,
default_model,
config,
});
Self { state, transport }
}
pub async fn run(mut self) -> Result<()> {
tracing::info!("acp server: ready");
while let Some(msg) = self.transport.next().await {
match msg {
ClientMessage::Request { id, method, params } => {
Self::dispatch_request(self.state.clone(), id, &method, params).await;
}
ClientMessage::Notification { method, params } => {
Self::dispatch_notification(&self.state, &method, params).await;
}
ClientMessage::Response { .. } => {}
}
}
tracing::info!("acp server: stdin closed, exiting");
Ok(())
}
async fn dispatch_request(state: Arc<ServerState>, id: Value, method: &str, params: Value) {
match method {
protocol::INITIALIZE => {
state.handle.respond(id, protocol::initialize_result());
}
protocol::SESSION_NEW => {
Self::session_new(state, id, None).await;
}
protocol::SESSION_LOAD => {
let session_id = params
.get("sessionId")
.and_then(Value::as_str)
.map(str::to_string);
match session_id {
Some(sid) => Self::session_new(state, id, Some(&sid)).await,
None => state.handle.respond_error(
id,
protocol::INVALID_PARAMS,
"session/load requires sessionId",
),
}
}
protocol::SESSION_SET_MODEL => {
Self::session_set_model(state, id, params).await;
}
protocol::SESSION_SET_MODE => {
Self::session_set_mode(state, id, params).await;
}
protocol::SESSION_COMPACT => {
Self::session_compact(state, id, params).await;
}
protocol::SESSION_PROMPT => {
Self::session_prompt(state, id, params).await;
}
other => {
state.handle.respond_error(
id,
protocol::METHOD_NOT_FOUND,
format!("method not found: {other}"),
);
}
}
}
async fn dispatch_notification(state: &Arc<ServerState>, method: &str, params: Value) {
match method {
protocol::SESSION_CANCEL => {
if let Some(st) = Self::lookup(state, ¶ms).await
&& let Some(token) = st.active_cancel.lock().await.as_ref()
{
token.cancel();
}
}
protocol::SESSION_STEER | protocol::SESSION_STEER_LEGACY => {
if let (Some(st), Some(text)) = (
Self::lookup(state, ¶ms).await,
protocol::prompt_text(¶ms),
) {
state
.steer
.lock()
.await
.entry(st.id)
.or_default()
.push_back(QueuedUserMessage::plain(text));
}
}
other => {
tracing::debug!("acp server: ignoring unknown notification {other}");
}
}
}
async fn lookup(state: &Arc<ServerState>, params: &Value) -> Option<Arc<SessionState>> {
let sid = params.get("sessionId").and_then(Value::as_str)?;
state.states.lock().await.get(sid).cloned()
}
async fn session_new(state: Arc<ServerState>, id: Value, resume: Option<&str>) {
match resolve_or_create_session(&state.sessions, resume, "ACP").await {
Ok(session) => {
let acp_id = session.id.to_string();
let st = Arc::new(SessionState {
id: session.id,
model: Mutex::new(state.default_model.clone()),
mode: Mutex::new(protocol::AcpMode::default()),
active_cancel: Mutex::new(None),
});
state.states.lock().await.insert(acp_id.clone(), st.clone());
if resume.is_some() {
Self::restore_session_model(&state, &session, &st).await;
}
if resume.is_some()
&& let Ok(history) = state.messages.list_messages_for_session(session.id).await
{
for update in protocol::replay_updates(&history) {
state.handle.send(protocol::session_update(&acp_id, update));
}
let used = protocol::replay_usage(&history)
.or((session.token_count > 0).then_some(session.token_count));
if let Some(used) = used {
state.handle.send(protocol::session_update(
&acp_id,
json!({
"sessionUpdate": "usage",
"usage": {
"used": used,
"size": state.agent.context_limit_for_session(session.id),
},
}),
));
}
}
let current = st.model.lock().await.clone();
let models = catalog::models_payload(&state.config, current.as_deref());
let modes = protocol::modes_payload(*st.mode.lock().await);
state.handle.respond(
id,
json!({ "sessionId": acp_id, "models": models, "modes": modes }),
);
let commands = catalog::commands_payload();
if !commands.is_empty() {
state.handle.send(protocol::session_update(
&acp_id,
json!({
"sessionUpdate": "available_commands_update",
"availableCommands": commands,
}),
));
}
}
Err(e) => {
state
.handle
.respond_error(id, protocol::INVALID_PARAMS, format!("session: {e}"));
}
}
}
async fn session_set_model(state: Arc<ServerState>, id: Value, params: Value) {
let model = params.get("modelId").and_then(Value::as_str);
let (Some(st), Some(model)) = (Self::lookup(&state, ¶ms).await, model) else {
let msg = if model.is_none() {
"session/set_model requires modelId"
} else {
"session/set_model: unknown session"
};
state
.handle
.respond_error(id, protocol::INVALID_PARAMS, msg);
return;
};
if let Ok((provider_name, bare_model)) = parse_pair(model) {
match create_provider_by_name(&state.config, &provider_name).await {
Ok(provider) => {
state
.agent
.swap_provider_for_session(st.id, provider, bare_model.clone());
state.agent.mark_manual_switch(st.id, bare_model);
*st.model.lock().await = Some(model.to_string());
}
Err(e) => {
state.handle.respond_error(
id,
protocol::INVALID_PARAMS,
format!("session/set_model: {e}"),
);
return;
}
}
} else {
state.agent.set_session_model(st.id, model.to_string());
*st.model.lock().await = Some(model.to_string());
}
match state.sessions.get_session_required(st.id).await {
Ok(mut row) => {
match parse_pair(model) {
Ok((provider_name, bare_model)) => {
row.provider_name = Some(provider_name);
row.model = Some(bare_model);
}
Err(_) => row.model = Some(model.to_string()),
}
if let Err(e) = state.sessions.update_session(&row).await {
tracing::warn!("acp: model pick not persisted for {}: {e}", st.id);
}
}
Err(e) => tracing::warn!("acp: model pick not persisted for {}: {e}", st.id),
}
state.handle.respond(id, json!({}));
}
async fn session_set_mode(state: Arc<ServerState>, id: Value, params: Value) {
let mode_id = params.get("modeId").and_then(Value::as_str);
let (Some(st), Some(mode_id)) = (Self::lookup(&state, ¶ms).await, mode_id) else {
let msg = if mode_id.is_none() {
"session/set_mode requires modeId"
} else {
"session/set_mode: unknown session"
};
state
.handle
.respond_error(id, protocol::INVALID_PARAMS, msg);
return;
};
match protocol::AcpMode::parse(mode_id) {
Some(mode) => {
*st.mode.lock().await = mode;
state.handle.respond(id, json!({}));
}
None => state.handle.respond_error(
id,
protocol::INVALID_PARAMS,
format!("session/set_mode: unknown modeId '{mode_id}'"),
),
}
}
async fn restore_session_model(
state: &Arc<ServerState>,
session: &crate::db::models::Session,
st: &Arc<SessionState>,
) {
let model = session.model.clone().filter(|m| !m.trim().is_empty());
let provider_name = session
.provider_name
.clone()
.filter(|p| !p.trim().is_empty());
if let (Some(provider_name), Some(model)) = (provider_name, model.clone()) {
match create_provider_by_name(&state.config, &provider_name).await {
Ok(provider) => {
state
.agent
.swap_provider_for_session(session.id, provider, model.clone());
state.agent.mark_manual_switch(session.id, model.clone());
*st.model.lock().await = Some(format!("{provider_name}/{model}"));
return;
}
Err(e) => {
tracing::debug!("acp: provider restore skipped ({provider_name}): {e}");
}
}
}
if let Some(model) = model {
state.agent.set_session_model(session.id, model.clone());
*st.model.lock().await = Some(model);
}
}
async fn session_prompt(state: Arc<ServerState>, id: Value, params: Value) {
let Some(st) = Self::lookup(&state, ¶ms).await else {
state.handle.respond_error(
id,
protocol::INVALID_PARAMS,
"session/prompt: unknown session — call session/new first",
);
return;
};
let Some(text) = protocol::prompt_text(¶ms) else {
state.handle.respond_error(
id,
protocol::INVALID_PARAMS,
"session/prompt: empty prompt",
);
return;
};
let cancel = CancellationToken::new();
let mut guard = st.active_cancel.lock().await;
if guard.is_some() {
state.handle.respond_error(
id,
protocol::INVALID_REQUEST,
"turn already in progress for this session",
);
return;
}
*guard = Some(cancel.clone());
drop(guard);
tokio::spawn(turn::run_turn(state, st, id, text, cancel));
}
async fn session_compact(state: Arc<ServerState>, id: Value, params: Value) {
let Some(st) = Self::lookup(&state, ¶ms).await else {
state.handle.respond_error(
id,
protocol::INVALID_PARAMS,
"session/compact: unknown session — call session/new first",
);
return;
};
let cancel = CancellationToken::new();
let mut guard = st.active_cancel.lock().await;
if guard.is_some() {
state.handle.respond_error(
id,
protocol::INVALID_REQUEST,
"turn already in progress for this session",
);
return;
}
*guard = Some(cancel.clone());
drop(guard);
tokio::spawn(turn::run_turn(
state,
st,
id,
"[SYSTEM: Compact context now. Summarize this conversation for continuity.]"
.to_string(),
cancel,
));
}
}