use std::collections::HashSet;
use std::sync::Arc;
use anyhow::Result;
use everruns_core::command::ExecuteCommandRequest;
use everruns_core::message::ContentPart;
use everruns_core::tools::Tool;
use everruns_core::typed_id::SessionId;
use tokio::sync::{broadcast, mpsc, oneshot};
use crate::exec::tools::{BashTool, Workspace};
use crate::runtime::{ModelState, RuntimeHandles};
use crate::tui::transcript::{
Author, ChatLine, DeltaRouter, TurnEvent, assistant_lines_since, handle_live_event,
lines_for_event_with_router, lines_for_replayed_event, remember_write_todos_args,
shell_result_lines, status_for_event, tokens_for_event,
};
pub(crate) struct CommandOutcome {
pub success: bool,
pub message: String,
}
pub(crate) struct TurnHandle {
pub events: mpsc::UnboundedReceiver<TurnEvent>,
pub cancel: oneshot::Sender<()>,
}
#[derive(Clone)]
pub(crate) struct Session {
handles: RuntimeHandles,
model: ModelState,
}
impl Session {
pub fn new(handles: RuntimeHandles, model: ModelState) -> Self {
Self { handles, model }
}
pub fn session_id(&self) -> SessionId {
self.handles.session_id
}
pub(crate) fn report_herdr_state(&self, state: crate::capabilities::herdr::HerdrState) {
self.handles.report_herdr_state(state);
}
pub async fn reload_mcp_servers(&self) -> Result<Vec<String>> {
self.handles.reload_mcp_servers().await
}
pub fn connections(&self) -> Arc<crate::connectors::ConnectionStore> {
self.handles.connections.clone()
}
pub async fn activate_capability(
&self,
capability_id: &str,
) -> Result<everruns_runtime::CapabilityDelta> {
self.handles.activate_capability(capability_id).await
}
pub async fn deactivate_capability(
&self,
capability_id: &str,
) -> Result<everruns_runtime::CapabilityDelta> {
self.handles.deactivate_capability(capability_id).await
}
pub async fn replayed_lines(&self, count: usize) -> Result<Vec<ChatLine>> {
let events = self.handles.runtime.events().await?;
Ok(events
.iter()
.take(count)
.flat_map(lines_for_replayed_event)
.collect())
}
pub async fn active_lines(&self) -> Result<Vec<ChatLine>> {
Ok(self
.handles
.runtime
.events()
.await?
.iter()
.flat_map(lines_for_replayed_event)
.collect())
}
pub fn take_checkpoint_notice(&self) -> Option<String> {
self.handles.checkpoints.take_notice()
}
pub fn take_restored_prompt(&self) -> Option<String> {
self.handles.checkpoints.take_restored_prompt()
}
pub async fn execute_command(
&self,
name: &str,
arguments: Option<String>,
) -> Result<CommandOutcome> {
let request = ExecuteCommandRequest {
name: name.to_string(),
arguments,
controls: None,
};
let result = self
.handles
.runtime
.execute_command(self.handles.session_id, request)
.await?;
Ok(CommandOutcome {
success: result.success,
message: result.message,
})
}
pub fn run_turn(&self, prompt: String, images: Vec<ContentPart>) -> TurnHandle {
let handles = self.handles.clone();
let model = self.model.clone();
let (tx, rx) = mpsc::unbounded_channel::<TurnEvent>();
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
let mut live = handles.events.subscribe();
tokio::spawn(async move {
let session_id = handles.session_id;
let before = match handles.runtime.messages(session_id).await {
Ok(m) => m.len(),
Err(e) => {
let _ = tx.send(TurnEvent::Failed(format!("load history: {e}")));
let _ = tx.send(TurnEvent::Done);
return;
}
};
let events_before = match handles.runtime.events().await {
Ok(e) => e.len(),
Err(_) => 0,
};
let input = model.input_message_with_images(prompt.clone(), images);
let turn_handles = handles.clone();
let mut turn =
tokio::spawn(
async move { turn_handles.run_checkpointed_turn(&prompt, input).await },
);
let mut emitted_events = HashSet::new();
let mut delta_router = DeltaRouter::default();
let mut events_cursor = events_before;
let mut cancelled = false;
let mut joined = None;
loop {
tokio::select! {
biased;
_ = &mut cancel_rx => {
cancelled = true;
turn.abort();
break;
}
recv = live.recv() => match recv {
Ok(event) => {
if event.session_id != session_id {
continue;
}
handle_live_event(
&event,
&mut emitted_events,
&mut delta_router,
&tx,
);
}
Err(broadcast::error::RecvError::Lagged(_)) => {
live = handles.events.subscribe();
catch_up_events(
&handles,
&mut events_cursor,
&mut emitted_events,
&mut delta_router,
&tx,
)
.await;
}
Err(broadcast::error::RecvError::Closed) => break,
},
res = &mut turn => {
joined = Some(res);
break;
}
}
}
if cancelled {
handles.report_herdr_state(crate::capabilities::herdr::HerdrState::Idle);
let _ = tx.send(TurnEvent::Stream(None));
let _ = tx.send(TurnEvent::Lines(vec![ChatLine {
author: Author::System,
text: "turn cancelled".into(),
}]));
let _ = tx.send(TurnEvent::Done);
return;
}
catch_up_events(
&handles,
&mut events_cursor,
&mut emitted_events,
&mut delta_router,
&tx,
)
.await;
let _ = tx.send(TurnEvent::Stream(None));
let result = match joined {
Some(res) => res,
None => turn.await,
};
let result = match result {
Ok(result) => result,
Err(e) => {
let _ = tx.send(TurnEvent::Failed(format!("turn task: {e}")));
let _ = tx.send(TurnEvent::Done);
return;
}
};
let response = match result {
Ok(r) => r,
Err(e) => {
let _ = tx.send(TurnEvent::Failed(format!("{e}")));
let _ = tx.send(TurnEvent::Done);
return;
}
};
let messages = handles
.runtime
.messages(session_id)
.await
.unwrap_or_default();
let mut out = assistant_lines_since(&messages, before);
if out.is_empty() && !response.response.is_empty() {
out.push(ChatLine {
author: Author::Assistant,
text: response.response,
});
}
if !response.success
&& let Some(err) = response.error
{
out.push(ChatLine {
author: Author::System,
text: format!("turn error: {err}"),
});
}
let _ = tx.send(TurnEvent::Lines(out));
let _ = tx.send(TurnEvent::Done);
});
TurnHandle {
events: rx,
cancel: cancel_tx,
}
}
pub fn run_shell(
&self,
command: String,
workspace: Arc<crate::exec::workspace_host::WorkspaceHost>,
) -> TurnHandle {
let (tx, rx) = mpsc::unbounded_channel::<TurnEvent>();
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
let sandbox = self.handles.sandbox.clone();
let approval_gate = self.handles.sandbox_approval_gate.clone();
let approval_policy = self.handles.approval_policy;
tokio::spawn(async move {
let tool = BashTool::with_policy(
Workspace::new(workspace),
sandbox,
approval_policy,
approval_gate,
);
let run = tool.execute(serde_json::json!({
"command": command,
"output": "normal",
}));
tokio::select! {
result = run => {
let _ = tx.send(TurnEvent::Lines(shell_result_lines(result)));
}
_ = &mut cancel_rx => {
let _ = tx.send(TurnEvent::Lines(vec![ChatLine {
author: Author::System,
text: "turn cancelled".into(),
}]));
}
}
let _ = tx.send(TurnEvent::Done);
});
TurnHandle {
events: rx,
cancel: cancel_tx,
}
}
}
async fn catch_up_events(
handles: &RuntimeHandles,
cursor: &mut usize,
emitted_events: &mut HashSet<String>,
router: &mut DeltaRouter,
tx: &mpsc::UnboundedSender<TurnEvent>,
) {
let events = handles.runtime.events().await.unwrap_or_default();
let mut lines = Vec::new();
for event in events.iter().skip(*cursor) {
let event_id = event.id.to_string();
if !emitted_events.insert(event_id) {
continue;
}
if let Some(tokens) = tokens_for_event(event) {
let _ = tx.send(TurnEvent::Tokens(tokens));
}
remember_write_todos_args(event, router);
if let Some(activity) = status_for_event(event) {
let _ = tx.send(TurnEvent::Activity(activity));
}
lines.extend(lines_for_event_with_router(event, router));
}
*cursor = events.len();
if !lines.is_empty() {
let _ = tx.send(TurnEvent::Lines(lines));
}
}