use std::sync::Arc;
use async_trait::async_trait;
use crate::activity::cache::ActivityState;
pub use crate::core::sm::control::{
LaunchParams, RawObservation, SessionControl, SessionControlError, Submit, Summary,
};
use crate::daemon::managed_routes::{SpawnParams, record_to_json, resume_managed, spawn_managed};
use crate::daemon::state::DaemonState;
use crate::session_manager::{ManagedError, ManagedSessionId};
pub struct DaemonSessionControl {
state: Arc<DaemonState>,
}
impl DaemonSessionControl {
pub fn new(state: Arc<DaemonState>) -> Self {
Self { state }
}
fn parse_id(session_id: &str) -> Result<ManagedSessionId, SessionControlError> {
session_id
.parse::<uuid::Uuid>()
.map(ManagedSessionId::from)
.map_err(|_| SessionControlError::NotFound(session_id.to_string()))
}
fn map_managed_err(e: ManagedError) -> SessionControlError {
match e {
ManagedError::SessionNotFound(msg) => SessionControlError::NotFound(msg),
other => SessionControlError::Backend(other.to_string()),
}
}
}
#[async_trait]
impl SessionControl for DaemonSessionControl {
async fn launch(&self, params: LaunchParams) -> Result<serde_json::Value, SessionControlError> {
let task = params
.prompt
.filter(|p| !p.trim().is_empty())
.unwrap_or_else(|| "session-manager launched task".to_string());
let repo_url = params.repo_url.unwrap_or(params.workdir);
let git_ref = params.ref_.unwrap_or_default();
let spawn = SpawnParams {
repo_url,
git_ref,
task,
name_hint: None,
runtime: params.model,
ephemeral: params.ephemeral,
mcp_initiated: false,
inject_task: None,
deliverable_id: None,
force_new: false,
};
let record = spawn_managed(
&self.state,
crate::session_manager::ManagedSessionId::new(),
spawn,
)
.await
.map_err(SessionControlError::Backend)?;
Ok(serde_json::json!({ "session_id": record.id.to_string() }))
}
async fn list(&self) -> Result<serde_json::Value, SessionControlError> {
let mgr = self.state.session_manager().await;
let sessions: Vec<serde_json::Value> =
mgr.list().await.iter().map(record_to_json).collect();
Ok(serde_json::json!({ "sessions": sessions }))
}
async fn get(&self, session_id: &str) -> Result<serde_json::Value, SessionControlError> {
let id = Self::parse_id(session_id)?;
let mgr = self.state.session_manager().await;
let record = mgr.get(&id).await.map_err(Self::map_managed_err)?;
Ok(serde_json::json!({ "session": record_to_json(&record) }))
}
async fn send(
&self,
session_id: &str,
text: &str,
) -> Result<serde_json::Value, SessionControlError> {
let id = Self::parse_id(session_id)?;
let mgr = self.state.session_manager().await;
mgr.send_input(&id, text)
.await
.map_err(|e| SessionControlError::Backend(e.to_string()))?;
Ok(serde_json::json!({ "ok": true }))
}
async fn stop(&self, session_id: &str) -> Result<serde_json::Value, SessionControlError> {
let id = Self::parse_id(session_id)?;
let mgr = self.state.session_manager().await;
mgr.stop(&id).await.map_err(Self::map_managed_err)?;
Ok(serde_json::json!({ "ok": true }))
}
async fn resume(&self, session_id: &str) -> Result<serde_json::Value, SessionControlError> {
let id = Self::parse_id(session_id)?;
resume_managed(&self.state, &id)
.await
.map_err(|e| SessionControlError::Backend(e.to_string()))?;
Ok(serde_json::json!({ "ok": true }))
}
async fn kill(&self, session_id: &str) -> Result<serde_json::Value, SessionControlError> {
let id = Self::parse_id(session_id)?;
let mgr = self.state.session_manager().await;
mgr.decommission(&id, None)
.await
.map_err(Self::map_managed_err)?;
Ok(serde_json::json!({ "ok": true }))
}
async fn inject_text(
&self,
session_id: &str,
text: &str,
submit: Submit,
) -> Result<(), SessionControlError> {
let id = Self::parse_id(session_id)?;
let mgr = self.state.session_manager().await;
mgr.inject(&id, text, submit)
.await
.map_err(|e| SessionControlError::Backend(e.to_string()))
}
async fn observe(
&self,
session_id: &str,
lines: usize,
) -> Result<RawObservation, SessionControlError> {
let id = Self::parse_id(session_id)?;
let mgr = self.state.session_manager().await;
mgr.observe(&id, lines).await.map_err(Self::map_managed_err)
}
async fn summarize(&self, session_id: &str) -> Result<Summary, SessionControlError> {
let id = Self::parse_id(session_id)?;
let mgr = self.state.session_manager().await;
let pane_text = mgr.capture_pane(&id, 60).await.unwrap_or_default();
let monitor = self.state.activity_monitor();
let result = monitor
.check(session_id, &pane_text)
.await
.map_err(|e| SessionControlError::Backend(e.to_string()))?;
Ok(verdict_to_summary(result.verdict))
}
async fn adopt(
&self,
tmux_name: &str,
cwd: &str,
task: Option<&str>,
runtime: Option<&str>,
) -> Result<serde_json::Value, SessionControlError> {
let runtime_kind = match runtime {
None => crate::runtime::RuntimeKind::default(),
Some(raw) => raw
.parse::<crate::runtime::RuntimeKind>()
.map_err(|e| SessionControlError::Backend(e.to_string()))?,
};
let mgr = self.state.session_manager().await;
let record = mgr
.adopt_existing(
tmux_name,
std::path::PathBuf::from(cwd),
task.unwrap_or_default().to_string(),
runtime_kind,
false,
)
.await
.map_err(|e| match e {
ManagedError::TmuxSessionMissing(msg) => SessionControlError::NotFound(msg),
other => SessionControlError::Backend(other.to_string()),
})?;
Ok(serde_json::json!({ "session_id": record.id.to_string() }))
}
}
fn verdict_to_summary(verdict: crate::activity::cache::ActivityVerdict) -> Summary {
let summary = if verdict.state == ActivityState::Unknown {
None
} else {
Some(verdict.summary)
};
Summary {
state: verdict.state,
summary,
confidence: verdict.confidence,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::activity::cache::ActivityVerdict;
#[test]
fn verdict_to_summary_drops_unknown_summary_to_none() {
let v = ActivityVerdict {
state: ActivityState::Unknown,
summary: "OPENROUTER_API_KEY not configured".into(),
confidence: 0.0,
};
let s = verdict_to_summary(v);
assert_eq!(s.state, ActivityState::Unknown);
assert_eq!(s.summary, None);
assert_eq!(s.confidence, 0.0);
}
#[test]
fn verdict_to_summary_keeps_real_summary() {
let v = ActivityVerdict {
state: ActivityState::Working,
summary: "writing code".into(),
confidence: 0.87,
};
let s = verdict_to_summary(v);
assert_eq!(s.state, ActivityState::Working);
assert_eq!(s.summary.as_deref(), Some("writing code"));
assert!((s.confidence - 0.87).abs() < 1e-6);
}
}