Skip to main content

kcode_k1_chat_model_usage_session/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../Documentation.md")]
3
4use kcode_k1_chat_model_usage::{ModelUsageCorrelation, PendingModelUsage};
5use kcode_k1_chat_thread_durable_state::DurableThread;
6use kcode_k1_codex_adapter::{Adapter, Error as AdapterError, RuntimeObservation};
7use std::{error::Error, fmt, future};
8use tokio::sync::mpsc::{self, UnboundedReceiver};
9
10#[derive(Default)]
11pub struct ModelUsageSession {
12    correlation: ModelUsageCorrelation,
13    key: Option<String>,
14    receiver: Option<UnboundedReceiver<RuntimeObservation>>,
15}
16
17#[derive(Debug)]
18pub enum ModelUsageSessionError {
19    Subscription(AdapterError),
20    Persistence(String),
21    ConflictingKey { active: String, requested: String },
22    InvalidState(&'static str),
23}
24
25impl fmt::Display for ModelUsageSessionError {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Self::Subscription(error) => write!(formatter, "usage subscription failed: {error}"),
29            Self::Persistence(error) => write!(formatter, "usage persistence failed: {error}"),
30            Self::ConflictingKey { active, requested } => {
31                write!(
32                    formatter,
33                    "usage key {requested:?} conflicts with active key {active:?}"
34                )
35            }
36            Self::InvalidState(error) => write!(formatter, "invalid usage session state: {error}"),
37        }
38    }
39}
40
41impl Error for ModelUsageSessionError {}
42
43impl ModelUsageSession {
44    pub async fn subscribe(
45        &mut self,
46        adapter: &Adapter,
47        key: String,
48    ) -> Result<(), ModelUsageSessionError> {
49        if let Some(active) = self.active_key()? {
50            return if active == key {
51                Ok(())
52            } else {
53                Err(ModelUsageSessionError::ConflictingKey {
54                    active: active.into(),
55                    requested: key,
56                })
57            };
58        }
59        let receiver = adapter
60            .subscribe_usage(key.clone())
61            .await
62            .map_err(ModelUsageSessionError::Subscription)?;
63        self.key = Some(key);
64        self.receiver = Some(receiver);
65        Ok(())
66    }
67
68    pub async fn receive(
69        &mut self,
70        durable: &mut DurableThread,
71    ) -> Result<bool, ModelUsageSessionError> {
72        let Some(key) = self.active_key()?.map(str::to_owned) else {
73            return future::pending::<Result<bool, ModelUsageSessionError>>().await;
74        };
75        let observation = self
76            .receiver
77            .as_mut()
78            .ok_or(ModelUsageSessionError::InvalidState(
79                "active key without receiver",
80            ))?
81            .recv()
82            .await;
83        let Some(observation) = observation else {
84            self.close_receiver(durable, &key)?;
85            return Ok(false);
86        };
87        let ready = self.correlation.observe(&key, observation);
88        persist(durable, ready)?;
89        Ok(true)
90    }
91
92    pub async fn drain(
93        &mut self,
94        durable: &mut DurableThread,
95    ) -> Result<(), ModelUsageSessionError> {
96        let Some(key) = self.active_key()?.map(str::to_owned) else {
97            return Ok(());
98        };
99        loop {
100            let received = self
101                .receiver
102                .as_mut()
103                .ok_or(ModelUsageSessionError::InvalidState(
104                    "active key without receiver",
105                ))?
106                .try_recv();
107            match received {
108                Ok(observation) => {
109                    let ready = self.correlation.observe(&key, observation);
110                    persist(durable, ready)?;
111                }
112                Err(mpsc::error::TryRecvError::Empty) => return Ok(()),
113                Err(mpsc::error::TryRecvError::Disconnected) => {
114                    self.close_receiver(durable, &key)?;
115                    return Ok(());
116                }
117            }
118        }
119    }
120
121    pub async fn finish_inference(
122        &mut self,
123        durable: &mut DurableThread,
124        key: &str,
125        terminal_agent_response_id: Option<u64>,
126    ) -> Result<(), ModelUsageSessionError> {
127        self.drain(durable).await?;
128        self.require_active(key)?;
129        let ready = self
130            .correlation
131            .finish_inference(key, terminal_agent_response_id);
132        persist(durable, ready)
133    }
134
135    pub async fn restart(
136        &mut self,
137        durable: &mut DurableThread,
138    ) -> Result<(), ModelUsageSessionError> {
139        self.drain(durable).await?;
140        if let Some(key) = self.active_key()?.map(str::to_owned) {
141            let pending = self.correlation.abandon_key(&key);
142            persist(durable, pending)?;
143            self.clear_subscription();
144        }
145        Ok(())
146    }
147
148    pub async fn shutdown(
149        &mut self,
150        durable: &mut DurableThread,
151        active_inference: bool,
152    ) -> Result<(), ModelUsageSessionError> {
153        self.drain(durable).await?;
154        if let Some(key) = self.active_key()?.map(str::to_owned) {
155            if active_inference {
156                let ready = self.correlation.finish_inference(&key, None);
157                persist(durable, ready)?;
158            }
159            let pending = self.correlation.abandon_key(&key);
160            persist(durable, pending)?;
161        }
162        let pending = self.correlation.drain_unassociated();
163        persist(durable, pending)?;
164        self.clear_subscription();
165        Ok(())
166    }
167
168    pub fn is_subscribed(&self) -> bool {
169        self.key.is_some() && self.receiver.is_some()
170    }
171
172    fn active_key(&self) -> Result<Option<&str>, ModelUsageSessionError> {
173        match (&self.key, &self.receiver) {
174            (None, None) => Ok(None),
175            (Some(key), Some(_)) => Ok(Some(key)),
176            _ => Err(ModelUsageSessionError::InvalidState(
177                "key and receiver must be installed together",
178            )),
179        }
180    }
181
182    fn require_active(&self, key: &str) -> Result<(), ModelUsageSessionError> {
183        match self.active_key()? {
184            Some(active) if active == key => Ok(()),
185            Some(active) => Err(ModelUsageSessionError::ConflictingKey {
186                active: active.into(),
187                requested: key.into(),
188            }),
189            None => Err(ModelUsageSessionError::InvalidState(
190                "no active usage subscription",
191            )),
192        }
193    }
194
195    fn close_receiver(
196        &mut self,
197        durable: &mut DurableThread,
198        key: &str,
199    ) -> Result<(), ModelUsageSessionError> {
200        let pending = self.correlation.abandon_key(key);
201        persist(durable, pending)?;
202        self.clear_subscription();
203        Ok(())
204    }
205
206    fn clear_subscription(&mut self) {
207        self.key = None;
208        self.receiver = None;
209    }
210}
211
212fn persist(
213    durable: &mut DurableThread,
214    pending: Vec<PendingModelUsage>,
215) -> Result<(), ModelUsageSessionError> {
216    for record in pending {
217        durable
218            .record_model_usage(record.connected_box_id, record.usage)
219            .map_err(ModelUsageSessionError::Persistence)?;
220    }
221    Ok(())
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn default_session_is_unsubscribed() {
230        assert!(!ModelUsageSession::default().is_subscribed());
231    }
232
233    #[test]
234    fn conflicting_key_error_names_both_keys() {
235        let error = ModelUsageSessionError::ConflictingKey {
236            active: "one".into(),
237            requested: "two".into(),
238        };
239        assert_eq!(
240            error.to_string(),
241            "usage key \"two\" conflicts with active key \"one\""
242        );
243    }
244}