kcode-k1-chat-model-usage-session 0.1.0

Manages model-usage subscriptions and durable persistence for K1 chat sessions.
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../Documentation.md")]

use kcode_k1_chat_model_usage::{ModelUsageCorrelation, PendingModelUsage};
use kcode_k1_chat_thread_durable_state::DurableThread;
use kcode_k1_codex_adapter::{Adapter, Error as AdapterError, RuntimeObservation};
use std::{error::Error, fmt, future};
use tokio::sync::mpsc::{self, UnboundedReceiver};

#[derive(Default)]
pub struct ModelUsageSession {
    correlation: ModelUsageCorrelation,
    key: Option<String>,
    receiver: Option<UnboundedReceiver<RuntimeObservation>>,
}

#[derive(Debug)]
pub enum ModelUsageSessionError {
    Subscription(AdapterError),
    Persistence(String),
    ConflictingKey { active: String, requested: String },
    InvalidState(&'static str),
}

impl fmt::Display for ModelUsageSessionError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Subscription(error) => write!(formatter, "usage subscription failed: {error}"),
            Self::Persistence(error) => write!(formatter, "usage persistence failed: {error}"),
            Self::ConflictingKey { active, requested } => {
                write!(
                    formatter,
                    "usage key {requested:?} conflicts with active key {active:?}"
                )
            }
            Self::InvalidState(error) => write!(formatter, "invalid usage session state: {error}"),
        }
    }
}

impl Error for ModelUsageSessionError {}

impl ModelUsageSession {
    pub async fn subscribe(
        &mut self,
        adapter: &Adapter,
        key: String,
    ) -> Result<(), ModelUsageSessionError> {
        if let Some(active) = self.active_key()? {
            return if active == key {
                Ok(())
            } else {
                Err(ModelUsageSessionError::ConflictingKey {
                    active: active.into(),
                    requested: key,
                })
            };
        }
        let receiver = adapter
            .subscribe_usage(key.clone())
            .await
            .map_err(ModelUsageSessionError::Subscription)?;
        self.key = Some(key);
        self.receiver = Some(receiver);
        Ok(())
    }

    pub async fn receive(
        &mut self,
        durable: &mut DurableThread,
    ) -> Result<bool, ModelUsageSessionError> {
        let Some(key) = self.active_key()?.map(str::to_owned) else {
            return future::pending::<Result<bool, ModelUsageSessionError>>().await;
        };
        let observation = self
            .receiver
            .as_mut()
            .ok_or(ModelUsageSessionError::InvalidState(
                "active key without receiver",
            ))?
            .recv()
            .await;
        let Some(observation) = observation else {
            self.close_receiver(durable, &key)?;
            return Ok(false);
        };
        let ready = self.correlation.observe(&key, observation);
        persist(durable, ready)?;
        Ok(true)
    }

    pub async fn drain(
        &mut self,
        durable: &mut DurableThread,
    ) -> Result<(), ModelUsageSessionError> {
        let Some(key) = self.active_key()?.map(str::to_owned) else {
            return Ok(());
        };
        loop {
            let received = self
                .receiver
                .as_mut()
                .ok_or(ModelUsageSessionError::InvalidState(
                    "active key without receiver",
                ))?
                .try_recv();
            match received {
                Ok(observation) => {
                    let ready = self.correlation.observe(&key, observation);
                    persist(durable, ready)?;
                }
                Err(mpsc::error::TryRecvError::Empty) => return Ok(()),
                Err(mpsc::error::TryRecvError::Disconnected) => {
                    self.close_receiver(durable, &key)?;
                    return Ok(());
                }
            }
        }
    }

    pub async fn finish_inference(
        &mut self,
        durable: &mut DurableThread,
        key: &str,
        terminal_agent_response_id: Option<u64>,
    ) -> Result<(), ModelUsageSessionError> {
        self.drain(durable).await?;
        self.require_active(key)?;
        let ready = self
            .correlation
            .finish_inference(key, terminal_agent_response_id);
        persist(durable, ready)
    }

    pub async fn restart(
        &mut self,
        durable: &mut DurableThread,
    ) -> Result<(), ModelUsageSessionError> {
        self.drain(durable).await?;
        if let Some(key) = self.active_key()?.map(str::to_owned) {
            let pending = self.correlation.abandon_key(&key);
            persist(durable, pending)?;
            self.clear_subscription();
        }
        Ok(())
    }

    pub async fn shutdown(
        &mut self,
        durable: &mut DurableThread,
        active_inference: bool,
    ) -> Result<(), ModelUsageSessionError> {
        self.drain(durable).await?;
        if let Some(key) = self.active_key()?.map(str::to_owned) {
            if active_inference {
                let ready = self.correlation.finish_inference(&key, None);
                persist(durable, ready)?;
            }
            let pending = self.correlation.abandon_key(&key);
            persist(durable, pending)?;
        }
        let pending = self.correlation.drain_unassociated();
        persist(durable, pending)?;
        self.clear_subscription();
        Ok(())
    }

    pub fn is_subscribed(&self) -> bool {
        self.key.is_some() && self.receiver.is_some()
    }

    fn active_key(&self) -> Result<Option<&str>, ModelUsageSessionError> {
        match (&self.key, &self.receiver) {
            (None, None) => Ok(None),
            (Some(key), Some(_)) => Ok(Some(key)),
            _ => Err(ModelUsageSessionError::InvalidState(
                "key and receiver must be installed together",
            )),
        }
    }

    fn require_active(&self, key: &str) -> Result<(), ModelUsageSessionError> {
        match self.active_key()? {
            Some(active) if active == key => Ok(()),
            Some(active) => Err(ModelUsageSessionError::ConflictingKey {
                active: active.into(),
                requested: key.into(),
            }),
            None => Err(ModelUsageSessionError::InvalidState(
                "no active usage subscription",
            )),
        }
    }

    fn close_receiver(
        &mut self,
        durable: &mut DurableThread,
        key: &str,
    ) -> Result<(), ModelUsageSessionError> {
        let pending = self.correlation.abandon_key(key);
        persist(durable, pending)?;
        self.clear_subscription();
        Ok(())
    }

    fn clear_subscription(&mut self) {
        self.key = None;
        self.receiver = None;
    }
}

fn persist(
    durable: &mut DurableThread,
    pending: Vec<PendingModelUsage>,
) -> Result<(), ModelUsageSessionError> {
    for record in pending {
        durable
            .record_model_usage(record.connected_box_id, record.usage)
            .map_err(ModelUsageSessionError::Persistence)?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_session_is_unsubscribed() {
        assert!(!ModelUsageSession::default().is_subscribed());
    }

    #[test]
    fn conflicting_key_error_names_both_keys() {
        let error = ModelUsageSessionError::ConflictingKey {
            active: "one".into(),
            requested: "two".into(),
        };
        assert_eq!(
            error.to_string(),
            "usage key \"two\" conflicts with active key \"one\""
        );
    }
}