use agent_client_protocol::schema::v1::{
ContentBlock, ContentChunk, PromptResponse, SessionNotification, SessionUpdate, StopReason,
};
use rho_sdk::{CancellationToken, CompactionOutcome};
use super::{send_notification, SessionHost};
use crate::app::acp::{
compaction_card::{outcome_summary, CompactionCard},
AcpClientPort,
};
impl SessionHost {
pub(super) async fn compact(
&mut self,
instructions: Option<String>,
client: &dyn AcpClientPort,
) -> anyhow::Result<PromptResponse> {
let card = self.host_updates.compaction.then(CompactionCard::new);
if let Some(card) = &card {
self.send_update(client, card.in_progress()).await?;
}
let result = self.run_compaction(instructions, client).await;
let report = match (card, &result) {
(Some(card), Ok(Some(_))) => Some(card.completed()),
(Some(card), Ok(None)) => Some(card.cancelled()),
(Some(card), Err(error)) => Some(card.failed(error.to_string())),
(None, Ok(Some(outcome))) => Some(SessionUpdate::AgentMessageChunk(ContentChunk::new(
ContentBlock::from(outcome_summary(outcome)),
))),
(None, Ok(None) | Err(_)) => None,
};
if let Some(report) = report {
let sent = self.send_update(client, report).await;
match (&result, sent) {
(Ok(Some(_)), sent) => sent?,
(Ok(None) | Err(_), Err(error)) => {
tracing::debug!(%error, "could not send closing compaction card");
}
(Ok(None) | Err(_), Ok(())) => {}
}
}
let stop = match result? {
Some(_) => StopReason::EndTurn,
None => StopReason::Cancelled,
};
Ok(PromptResponse::new(stop))
}
async fn run_compaction(
&mut self,
instructions: Option<String>,
client: &dyn AcpClientPort,
) -> anyhow::Result<Option<CompactionOutcome>> {
let cancel = CancellationToken::new();
self.prompt_gate.activate(cancel.clone());
let session = self.built.session.clone();
let mut task = tokio::spawn(async move {
match instructions {
Some(instructions) => session.compact_with_instructions(instructions).await,
None => session.compact().await,
}
});
let joined = tokio::select! {
biased;
() = cancel.cancelled() => {
task.abort();
(&mut task).await
}
joined = &mut task => joined,
};
let outcome = match joined {
Ok(outcome) => outcome.map_err(|error| {
let message = format!("could not compact context: {error}");
anyhow::Error::new(error).context(message)
})?,
Err(error) if error.is_cancelled() => return Ok(None),
Err(error) => return Err(anyhow::anyhow!("could not compact context: {error}")),
};
if self.is_replaced() {
return Ok(None);
}
let snapshot = self.durable_snapshot();
let saved = self
.stored()
.and_then(|stored| stored.save_compaction_snapshot(&snapshot, &[], &outcome));
if let Err(error) = saved {
self.unsaved_compaction = true;
return Err(error.context("could not save compacted context"));
}
let signals = self.signals();
signals.send_usage(&self.acp_session_id, client).await;
self.sent_plan = signals.into_sent_plan();
Ok(Some(outcome))
}
async fn send_update(
&self,
client: &dyn AcpClientPort,
update: SessionUpdate,
) -> anyhow::Result<()> {
send_notification(
client,
SessionNotification::new(self.acp_session_id.clone(), update),
)
.await
}
}