use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex as StdMutex};
use serde_json::{Value, json};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::brain::agent::{ApprovalCallback, ProgressCallback, ProgressEvent, ToolApprovalInfo};
use crate::brain::provider::StopReason;
use super::protocol::{self, permission_options, permission_outcome, session_update, text_chunk};
use super::server::{ServerState, SessionState};
use super::transport::TransportHandle;
pub async fn run_turn(
state: Arc<ServerState>,
session: Arc<SessionState>,
request_id: Value,
text: String,
cancel: CancellationToken,
) {
let acp_session_id = session.id.to_string();
let progress = progress_callback(
state.handle.clone(),
acp_session_id.clone(),
state.agent.clone(),
);
let mode = *session.mode.lock().await;
let approval = approval_callback(state.handle.clone(), acp_session_id, mode, cancel.clone());
let model = session.model.lock().await.clone();
let result = state
.agent
.send_message_with_tools_and_callback(
session.id,
text,
model,
Some(cancel.clone()),
Some(approval),
Some(progress),
"acp",
None,
None,
)
.await;
*session.active_cancel.lock().await = None;
match result {
Ok(response) => {
let stop = if cancel.is_cancelled() {
"cancelled"
} else {
stop_reason(response.stop_reason)
};
state
.handle
.respond(request_id, json!({ "stopReason": stop }));
}
Err(_) if cancel.is_cancelled() => {
state
.handle
.respond(request_id, json!({ "stopReason": "cancelled" }));
}
Err(e) => {
tracing::warn!("acp turn failed: {e:#}");
state.handle.respond_error(
request_id,
protocol::INTERNAL_ERROR,
crate::brain::agent::format_user_error(&e),
);
}
}
}
pub(crate) fn stop_reason(reason: Option<StopReason>) -> &'static str {
match reason {
Some(StopReason::MaxTokens) => "max_tokens",
Some(StopReason::StopSequence) => "end_turn",
_ => "end_turn",
}
}
pub(crate) fn round_text_is_duplicate(streamed: &str, aggregate: &str) -> bool {
!aggregate.trim().is_empty() && streamed.trim() == aggregate.trim()
}
fn progress_callback(
handle: TransportHandle,
acp_session_id: String,
agent: Arc<crate::brain::agent::AgentService>,
) -> ProgressCallback {
let open_calls: Arc<StdMutex<HashMap<String, VecDeque<String>>>> =
Arc::new(StdMutex::new(HashMap::new()));
let streamed: Arc<StdMutex<String>> = Arc::new(StdMutex::new(String::new()));
let plan_seq = Arc::new(std::sync::atomic::AtomicU64::new(0));
Arc::new(move |session_id, event| {
let update = match event {
ProgressEvent::StreamingChunk { text } => {
if let Ok(mut buf) = streamed.lock() {
buf.push_str(&text);
}
Some(text_chunk("agent_message_chunk", &text))
}
ProgressEvent::IntermediateText { text, .. } => {
let duplicate = streamed
.lock()
.map(|mut buf| {
let dup = round_text_is_duplicate(&buf, &text);
buf.clear();
dup
})
.unwrap_or(false);
if duplicate {
None
} else {
Some(text_chunk("agent_message_chunk", &text))
}
}
ProgressEvent::ReasoningChunk { text } => {
Some(text_chunk("agent_thought_chunk", &text))
}
ProgressEvent::ToolStarted {
tool_name,
tool_input,
..
} => {
if let Ok(mut buf) = streamed.lock() {
buf.clear();
}
let call_id = Uuid::new_v4().to_string();
if let Ok(mut calls) = open_calls.lock() {
calls
.entry(tool_name.clone())
.or_default()
.push_back(call_id.clone());
}
Some(tool_call_frame(&call_id, &tool_name, &tool_input))
}
ProgressEvent::ToolCompleted {
tool_name,
success,
summary,
..
} => {
let call_id = open_calls
.lock()
.ok()
.and_then(|mut calls| calls.get_mut(&tool_name).and_then(VecDeque::pop_front))
.unwrap_or_else(|| Uuid::new_v4().to_string());
if tool_name == "plan" && success {
let handle = handle.clone();
let acp_id = acp_session_id.clone();
let seq = plan_seq.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
let plan_seq = plan_seq.clone();
tokio::spawn(async move {
let doc = crate::utils::plan_files::load_plan(session_id).await;
if plan_seq.load(std::sync::atomic::Ordering::SeqCst) != seq {
return;
}
let entries = doc.as_ref().map(plan_entries).unwrap_or_default();
handle.send(session_update(&acp_id, protocol::plan_update(entries)));
});
}
Some(tool_call_update_frame(
&call_id, &tool_name, success, &summary,
))
}
ProgressEvent::TokenCount(used) => Some(protocol::usage_update(
used as u64,
agent.context_limit_for_session(session_id) as u64,
)),
_ => None,
};
if let Some(update) = update {
handle.send(session_update(&acp_session_id, update));
}
})
}
pub(crate) fn tool_call_frame(call_id: &str, tool_name: &str, tool_input: &Value) -> Value {
json!({
"sessionUpdate": "tool_call",
"toolCallId": call_id,
"title": tool_name,
"name": tool_name,
"kind": protocol::tool_kind(tool_name),
"status": "in_progress",
"rawInput": tool_input,
})
}
pub(crate) fn tool_call_update_frame(
call_id: &str,
tool_name: &str,
success: bool,
summary: &str,
) -> Value {
json!({
"sessionUpdate": "tool_call_update",
"toolCallId": call_id,
"name": tool_name,
"status": if success { "completed" } else { "failed" },
"rawOutput": summary,
})
}
pub(crate) fn plan_entries(doc: &crate::tui::plan::PlanDocument) -> Vec<Value> {
doc.tasks
.iter()
.map(|task| {
let status = match task.status {
crate::tui::plan::TaskStatus::Completed => "completed",
crate::tui::plan::TaskStatus::InProgress => "in_progress",
_ => "pending",
};
let priority = match task.complexity {
4..=u8::MAX => "high",
3 => "medium",
_ => "low",
};
json!({
"content": if task.title.is_empty() {
&task.description
} else {
&task.title
},
"priority": priority,
"status": status,
"_meta": {
"order": task.order,
"taskType": task.task_type.to_string(),
"status": task.status.to_string(),
"complexity": task.complexity,
},
})
})
.collect()
}
const PERMISSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
fn approval_callback(
handle: TransportHandle,
acp_session_id: String,
mode: protocol::AcpMode,
cancel: CancellationToken,
) -> ApprovalCallback {
Arc::new(move |info: ToolApprovalInfo| {
let handle = handle.clone();
let acp_session_id = acp_session_id.clone();
let cancel = cancel.clone();
Box::pin(async move {
let kind = protocol::tool_kind(&info.tool_name);
match mode {
protocol::AcpMode::Plan => {
tracing::info!("acp plan mode: denied {} without asking", info.tool_name);
return Ok((false, false));
}
protocol::AcpMode::Auto | protocol::AcpMode::FullAccess => {
return Ok((true, false));
}
protocol::AcpMode::AutoAcceptEdits if kind == "edit" => {
return Ok((true, false));
}
_ => {}
}
let params = json!({
"sessionId": acp_session_id,
"toolCall": {
"toolCallId": Uuid::new_v4().to_string(),
"title": info.tool_name.clone(),
"name": info.tool_name,
"kind": kind,
"rawInput": info.tool_input,
},
"options": permission_options(),
});
let call = handle.call(protocol::SESSION_REQUEST_PERMISSION, params);
let outcome = tokio::select! {
res = call => res,
_ = cancel.cancelled() => {
Err(anyhow::anyhow!("turn cancelled before the client answered"))
}
_ = tokio::time::sleep(PERMISSION_TIMEOUT) => {
Err(anyhow::anyhow!(
"no permission response within {PERMISSION_TIMEOUT:?}, denying"
))
}
};
match outcome {
Ok(result) => Ok(permission_outcome(&result)),
Err(e) => {
tracing::warn!("acp permission request failed, denying: {e:#}");
Ok((false, false))
}
}
})
})
}