kcode_k1_chat_model_usage_session/
lib.rs1#![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}