use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use futures_util::future::join_all;
use crate::log::{LogValue, Logger, fields};
use crate::session::event::{ReactionOutcome, SessionEvent, SessionUsage};
pub const DEFAULT_TRANSCRIPT_LIMIT: usize = 400;
pub const WITHDRAWN_NOTE: &str = "[withdrawn by the person who sent it]";
pub fn shown_text(withdrawn: bool, text: &str) -> &str {
if withdrawn { WITHDRAWN_NOTE } else { text }
}
#[derive(Debug, Clone, PartialEq)]
pub struct Held {
pub turn: Option<u32>,
pub entry: SessionEvent,
}
pub trait Recorder: Send + Sync {
fn append(&self, entry: &SessionEvent, turn: u32);
}
pub type ViewError = Box<dyn std::error::Error + Send + Sync>;
pub trait SessionView: Send + Sync {
fn observe<'a>(
&'a self,
event: &'a SessionEvent,
) -> Pin<Box<dyn Future<Output = Result<(), ViewError>> + Send + 'a>>;
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ViewState {
pub busy: bool,
pub waiting: Option<String>,
pub ended: bool,
pub usage: Option<SessionUsage>,
}
#[derive(Default)]
struct Inner {
views: Vec<Arc<dyn SessionView>>,
held: Vec<Held>,
reactions: Vec<(String, ReactionOutcome)>,
busy: bool,
waiting: Option<String>,
ended: bool,
dropped: usize,
usage: Option<SessionUsage>,
turn: u32,
}
impl Inner {
fn state(&self) -> ViewState {
ViewState {
busy: self.busy,
waiting: self.waiting.clone(),
ended: self.ended,
usage: self.usage.clone(),
}
}
}
struct Snapshot {
dropped: usize,
held: Vec<Held>,
reactions: Vec<(String, ReactionOutcome)>,
state: ViewState,
turn: u32,
}
fn label_of(event: &SessionEvent) -> &'static str {
match event {
SessionEvent::Post { .. } => "post",
SessionEvent::Prompt { .. } => "prompt",
SessionEvent::Aside { .. } => "aside",
SessionEvent::Notice { .. } => "notice",
SessionEvent::Thinking { .. } => "thinking",
SessionEvent::Reply { .. } => "reply",
SessionEvent::ToolResult { .. } => "toolResult",
SessionEvent::Activity { .. } => "activity",
SessionEvent::Delegation { .. } => "delegation",
SessionEvent::Diff { .. } => "diff",
SessionEvent::Attachment { .. } => "attachment",
SessionEvent::Upload { .. } => "upload",
SessionEvent::Usage { .. } => "usage",
SessionEvent::Waiting { .. } => "waiting",
SessionEvent::Reaction { .. } => "reaction",
SessionEvent::Busy { .. } => "busy",
SessionEvent::BeginTurn { .. } => "turn",
SessionEvent::Close { .. } => "close",
}
}
fn set_reaction(
reactions: &mut Vec<(String, ReactionOutcome)>,
message_id: &str,
outcome: ReactionOutcome,
) {
for (existing, value) in reactions.iter_mut() {
if existing == message_id {
*value = outcome;
return;
}
}
reactions.push((message_id.to_owned(), outcome));
}
pub struct ViewFanOut {
log: Logger,
limit: usize,
recorder: Option<Arc<dyn Recorder>>,
inner: Mutex<Inner>,
}
impl ViewFanOut {
#[allow(dead_code, reason = "the daemon builds every fan-out with_recorder")]
pub fn new(log: Logger) -> Self {
Self::with_recorder(log, DEFAULT_TRANSCRIPT_LIMIT, None)
}
pub fn with_recorder(log: Logger, limit: usize, recorder: Option<Arc<dyn Recorder>>) -> Self {
Self {
log,
limit,
recorder,
inner: Mutex::new(Inner::default()),
}
}
pub fn restore(&self, entries: &[Held], dropped: usize) {
let mut inner = self.inner.lock().expect("the view fan-out lock");
let kept_from = entries.len().saturating_sub(self.limit);
inner.held = entries[kept_from..]
.iter()
.filter(|held| !matches!(held.entry, SessionEvent::Usage { .. }))
.cloned()
.collect();
inner.dropped = dropped + kept_from;
inner.usage = entries.iter().rev().find_map(|held| match &held.entry {
SessionEvent::Usage { usage } => Some(usage.clone()),
_ => None,
});
inner.turn = entries
.iter()
.map(|held| held.turn.unwrap_or(0))
.max()
.unwrap_or(0);
}
pub fn current_turn(&self) -> u32 {
self.inner.lock().expect("the view fan-out lock").turn
}
#[allow(
dead_code,
reason = "read by this module's tests, which assert on state the daemon never asks for"
)]
pub fn size(&self) -> usize {
self.inner
.lock()
.expect("the view fan-out lock")
.views
.len()
}
pub fn state(&self) -> ViewState {
self.inner.lock().expect("the view fan-out lock").state()
}
#[allow(
dead_code,
reason = "read by this module's tests, which assert on state the daemon never asks for"
)]
pub fn history(&self) -> Vec<SessionEvent> {
let inner = self.inner.lock().expect("the view fan-out lock");
inner.held.iter().map(|held| held.entry.clone()).collect()
}
#[allow(
dead_code,
reason = "read by this module's tests, which assert on state the daemon never asks for"
)]
pub fn held(&self) -> Vec<Held> {
self.inner
.lock()
.expect("the view fan-out lock")
.held
.clone()
}
#[allow(
dead_code,
reason = "read by this module's tests, which assert on state the daemon never asks for"
)]
pub fn dropped_count(&self) -> usize {
self.inner.lock().expect("the view fan-out lock").dropped
}
pub async fn send(&self, event: SessionEvent) {
let views = {
let mut inner = self.inner.lock().expect("the view fan-out lock");
self.record(&mut inner, &event);
inner.views.clone()
};
self.deliver(&views, &event).await;
}
pub async fn attach(self: Arc<Self>, view: Arc<dyn SessionView>) -> Attached {
let snapshot = {
let mut inner = self.inner.lock().expect("the view fan-out lock");
if !inner
.views
.iter()
.any(|attached| Arc::ptr_eq(attached, &view))
{
inner.views.push(Arc::clone(&view));
}
Snapshot {
dropped: inner.dropped,
held: inner.held.clone(),
reactions: inner.reactions.clone(),
state: inner.state(),
turn: inner.turn,
}
};
if let Err(error) = Self::replay_to(&view, &snapshot).await {
self.log.warn(
"replaying to a new view failed",
&fields([("detail", error.to_string().into())]),
);
}
Attached { fan: self, view }
}
pub fn detach(&self, view: &Arc<dyn SessionView>) {
self.inner
.lock()
.expect("the view fan-out lock")
.views
.retain(|attached| !Arc::ptr_eq(attached, view));
}
fn record(&self, inner: &mut Inner, event: &SessionEvent) {
match event {
SessionEvent::BeginTurn { turn } => {
inner.turn = *turn;
return;
}
SessionEvent::Waiting { text } => {
inner.waiting.clone_from(text);
return;
}
SessionEvent::Reaction {
message_id,
outcome,
} => {
set_reaction(&mut inner.reactions, message_id, *outcome);
return;
}
SessionEvent::Busy { busy } => {
inner.busy = *busy;
return;
}
SessionEvent::Close { .. } => {
inner.ended = true;
inner.busy = false;
return;
}
SessionEvent::Reply { .. } => return,
SessionEvent::Usage { usage } => {
inner.usage = Some(usage.clone());
if let Some(recorder) = &self.recorder {
recorder.append(event, inner.turn);
}
return;
}
_ => {}
}
let entry = match event {
SessionEvent::Upload { name, bytes, .. } => SessionEvent::Attachment {
name: name.clone(),
size: bytes.len() as u64,
},
other => other.clone(),
};
if let Some(recorder) = &self.recorder {
recorder.append(&entry, inner.turn);
}
inner.held.push(Held {
turn: Some(inner.turn),
entry,
});
let excess = inner.held.len().saturating_sub(self.limit);
if excess > 0 {
inner.held.drain(..excess);
inner.dropped += excess;
}
}
async fn deliver(&self, views: &[Arc<dyn SessionView>], event: &SessionEvent) {
let results = join_all(views.iter().map(|view| view.observe(event))).await;
for result in results {
if let Err(error) = result {
self.log.warn(
"a view failed and was skipped",
&fields([
("what", LogValue::from(label_of(event))),
("detail", LogValue::from(error.to_string())),
]),
);
}
}
}
async fn replay_to(view: &Arc<dyn SessionView>, snapshot: &Snapshot) -> Result<(), ViewError> {
if snapshot.dropped > 0 {
view.observe(&SessionEvent::Post {
text: format!("[{} earlier line(s) not kept]", snapshot.dropped),
})
.await?;
}
let mut announced: Option<u32> = None;
for held in &snapshot.held {
if let Some(turn) = held.turn
&& announced != Some(turn)
{
announced = Some(turn);
view.observe(&SessionEvent::BeginTurn { turn }).await?;
}
match &held.entry {
SessionEvent::Post { .. }
| SessionEvent::Notice { .. }
| SessionEvent::Thinking { .. }
| SessionEvent::Reply { .. }
| SessionEvent::ToolResult { .. }
| SessionEvent::Activity { .. }
| SessionEvent::Delegation { .. }
| SessionEvent::Diff { .. } => view.observe(&held.entry).await?,
SessionEvent::Prompt {
author,
text,
withdrawn,
..
}
| SessionEvent::Aside {
author,
text,
withdrawn,
..
} => {
view.observe(&SessionEvent::Prompt {
author: author.clone(),
text: shown_text(*withdrawn, text).to_owned(),
id: None,
withdrawn: false,
})
.await?;
}
SessionEvent::Attachment { name, size } => {
view.observe(&SessionEvent::Post {
text: format!("[attached {name}, {size} bytes]"),
})
.await?;
}
SessionEvent::Upload { .. }
| SessionEvent::Usage { .. }
| SessionEvent::Waiting { .. }
| SessionEvent::Reaction { .. }
| SessionEvent::Busy { .. }
| SessionEvent::BeginTurn { .. }
| SessionEvent::Close { .. } => {}
}
}
if announced.is_some() && announced != Some(snapshot.turn) {
view.observe(&SessionEvent::BeginTurn {
turn: snapshot.turn,
})
.await?;
}
for (message_id, outcome) in &snapshot.reactions {
view.observe(&SessionEvent::Reaction {
message_id: message_id.clone(),
outcome: *outcome,
})
.await?;
}
if let Some(usage) = &snapshot.state.usage {
view.observe(&SessionEvent::Usage {
usage: usage.clone(),
})
.await?;
}
if let Some(text) = &snapshot.state.waiting {
view.observe(&SessionEvent::Waiting {
text: Some(text.clone()),
})
.await?;
}
if snapshot.state.busy {
view.observe(&SessionEvent::Busy { busy: true }).await?;
}
if snapshot.state.ended {
view.observe(&SessionEvent::Post {
text: "[this session has ended]".to_owned(),
})
.await?;
}
Ok(())
}
}
pub struct Attached {
fan: Arc<ViewFanOut>,
view: Arc<dyn SessionView>,
}
impl Attached {
pub fn detach(self) {
self.fan.detach(&self.view);
}
}
#[cfg(test)]
mod tests;