use agent_client_protocol::schema::v1::{
Plan, PlanEntry, PlanEntryPriority, PlanEntryStatus, SessionId, SessionNotification,
SessionUpdate, UsageUpdate,
};
use rho_sdk::RunEvent;
use super::AcpClientPort;
use crate::tools::todo::{TodoItem, TodoState, TodoStatus};
pub(super) struct SessionSignals {
session: rho_sdk::Session,
todo: TodoState,
context_window: Option<u64>,
sent_plan: Vec<PlanEntry>,
}
impl SessionSignals {
pub(super) fn new(
session: rho_sdk::Session,
todo: TodoState,
context_window: Option<u64>,
sent_plan: Vec<PlanEntry>,
) -> Self {
Self {
session,
todo,
context_window,
sent_plan,
}
}
pub(super) fn observe(&mut self, event: &RunEvent) -> Option<SessionUpdate> {
match event {
RunEvent::StepStarted { .. } | RunEvent::CompactionCompleted { .. } => {
Some(self.usage())
}
RunEvent::ToolFinished { .. } => self.plan_change(),
_ => None,
}
}
pub(super) fn usage(&self) -> SessionUpdate {
let estimate = self.session.context_estimate();
let size = self
.context_window
.or(estimate.reported_context_window())
.unwrap_or(0);
SessionUpdate::UsageUpdate(UsageUpdate::new(estimate.tokens(), size))
}
pub(super) async fn send_plan_change(
&mut self,
session_id: &SessionId,
client: &dyn AcpClientPort,
) {
if let Some(update) = self.plan_change() {
send(session_id, client, update).await;
}
}
pub(super) async fn send_usage(&self, session_id: &SessionId, client: &dyn AcpClientPort) {
send(session_id, client, self.usage()).await;
}
pub(super) fn into_sent_plan(self) -> Vec<PlanEntry> {
self.sent_plan
}
fn plan_change(&mut self) -> Option<SessionUpdate> {
let entries = plan_entries(&self.todo);
if entries == self.sent_plan {
return None;
}
self.sent_plan.clone_from(&entries);
Some(SessionUpdate::Plan(Plan::new(entries)))
}
}
async fn send(session_id: &SessionId, client: &dyn AcpClientPort, update: SessionUpdate) {
let notification = SessionNotification::new(session_id.clone(), update);
if let Err(error) = client.send_session_notification(notification).await {
tracing::debug!(%error, "could not send ACP session update");
}
}
fn plan_entries(todo: &TodoState) -> Vec<PlanEntry> {
todo.list()
.map(|list| list.todos.iter().map(plan_entry).collect())
.unwrap_or_default()
}
fn plan_entry(item: &TodoItem) -> PlanEntry {
let status = match item.status {
TodoStatus::Pending => PlanEntryStatus::Pending,
TodoStatus::InProgress => PlanEntryStatus::InProgress,
TodoStatus::Completed => PlanEntryStatus::Completed,
};
PlanEntry::new(item.content.clone(), PlanEntryPriority::Medium, status)
}
#[cfg(test)]
#[path = "signals_tests.rs"]
mod tests;