Skip to main content

kcode_k1_chat_thread_session_provider/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../Documentation.md")]
3
4use kcode_k1_chat_model_usage_session::ModelUsageSession;
5use kcode_k1_chat_thread_actor_channel::{
6    ActorError, ActorShim, Message, ProviderInput, ProviderInputKind, new_shim,
7};
8use kcode_k1_chat_thread_durable_state::{DurableThread, PreparedMailboxFlush};
9use kcode_k1_chat_thread_session_view::SessionView;
10use kcode_k1_codex_adapter::Adapter;
11use tokio::{sync::mpsc, task::JoinHandle};
12
13pub struct RestartPlan {
14    generation: u64,
15    active_key: String,
16}
17
18pub struct SessionProvider {
19    adapter: Adapter,
20    base_key: String,
21    active_key: String,
22    generation: u64,
23    shim: Option<ActorShim>,
24    sender: mpsc::UnboundedSender<Message>,
25    inference: Option<JoinHandle<()>>,
26    mailbox_flush: Option<JoinHandle<()>>,
27    job: Option<u64>,
28}
29
30impl SessionProvider {
31    pub fn new(
32        adapter: Adapter,
33        base_key: impl Into<String>,
34        sender: mpsc::UnboundedSender<Message>,
35    ) -> Self {
36        let base_key = base_key.into();
37        let active_key = base_key.clone();
38        let shim = Some(new_shim(
39            adapter.clone(),
40            active_key.clone(),
41            sender.clone(),
42        ));
43        Self {
44            adapter,
45            base_key,
46            active_key,
47            generation: 0,
48            shim,
49            sender,
50            inference: None,
51            mailbox_flush: None,
52            job: None,
53        }
54    }
55
56    pub fn job(&self) -> Option<u64> {
57        self.job
58    }
59
60    pub fn active_key(&self) -> &str {
61        &self.active_key
62    }
63
64    pub fn inference_active(&self) -> bool {
65        self.inference.is_some()
66    }
67
68    pub fn mailbox_flush_active(&self) -> bool {
69        self.mailbox_flush.is_some()
70    }
71
72    pub async fn drive(
73        &mut self,
74        durable: &mut DurableThread,
75        usage: &mut ModelUsageSession,
76        view: &mut SessionView,
77    ) -> Result<(), String> {
78        if self.inference.is_some() || self.shim.is_none() {
79            return Ok(());
80        }
81        let Some((job, input)) = durable.begin_input()? else {
82            return Ok(());
83        };
84        if !usage.is_subscribed() {
85            usage
86                .subscribe(&self.adapter, self.active_key.clone())
87                .await
88                .map_err(|_| "model-usage subscription failed".to_owned())?;
89        }
90        view.set_model_input(ProviderInput {
91            kind: ProviderInputKind::Turn,
92            text: input.clone(),
93        });
94        let mut shim = self.shim.take().expect("shim availability was checked");
95        self.job = Some(job);
96        let sender = self.sender.clone();
97        self.inference = Some(tokio::spawn(async move {
98            let result = shim.infer(input).await.map_err(|error| error.to_string());
99            let _ = sender.send(Message::Inferred(job, shim, result));
100        }));
101        Ok(())
102    }
103
104    pub fn launch_mailbox_flush(
105        &mut self,
106        durable: &DurableThread,
107        view: &mut SessionView,
108        prepared: PreparedMailboxFlush,
109    ) -> Result<(), String> {
110        if self.mailbox_flush.is_some() {
111            return Err("overlapping Codex mailbox flushes".to_owned());
112        }
113        let job = self
114            .job
115            .ok_or_else(|| "no active K1 inference".to_owned())?;
116        let input = durable.prepared_input(&prepared)?;
117        view.set_model_input(ProviderInput {
118            kind: ProviderInputKind::MailboxFlush,
119            text: input.clone(),
120        });
121        let adapter = self.adapter.clone();
122        let key = self.active_key.clone();
123        let sender = self.sender.clone();
124        self.mailbox_flush = Some(tokio::spawn(async move {
125            let result = adapter
126                .steer(key, input)
127                .await
128                .map_err(|error| error.to_string());
129            let _ = sender.send(Message::MailboxFlushCompleted(job, prepared, result));
130        }));
131        Ok(())
132    }
133
134    pub fn accept_mailbox_flush(
135        &mut self,
136        durable: &mut DurableThread,
137        job: u64,
138        prepared: PreparedMailboxFlush,
139        result: Result<(), String>,
140    ) -> Result<(), String> {
141        if self.mailbox_flush.is_none() || self.job != Some(job) {
142            return Err("stale Codex mailbox-flush completion".to_owned());
143        }
144        let _ = self.mailbox_flush.take();
145        result?;
146        durable.commit_mailbox_flush(prepared)
147    }
148
149    pub fn accept_inferred(&mut self, job: u64) -> Result<(), String> {
150        if self.inference.is_none() || self.mailbox_flush.is_some() || self.job != Some(job) {
151            return Err("stale Codex inference completion".to_owned());
152        }
153        let _ = self.inference.take();
154        Ok(())
155    }
156
157    pub fn settle_success(&mut self, shim: ActorShim) {
158        self.shim = Some(shim);
159        self.job = None;
160    }
161
162    pub fn settle_failure(&mut self) {
163        self.shim = None;
164        self.job = None;
165    }
166
167    pub fn prepare_restart(&self) -> Result<RestartPlan, ActorError> {
168        let generation = self
169            .generation
170            .checked_add(1)
171            .ok_or(ActorError::NotRestartable)?;
172        Ok(RestartPlan {
173            generation,
174            active_key: format!("{}#restart-{generation}", self.base_key),
175        })
176    }
177
178    pub fn install_restart(&mut self, plan: RestartPlan) {
179        self.generation = plan.generation;
180        self.active_key = plan.active_key;
181        self.shim = Some(new_shim(
182            self.adapter.clone(),
183            self.active_key.clone(),
184            self.sender.clone(),
185        ));
186    }
187
188    pub fn abort(&mut self) {
189        for task in [self.inference.take(), self.mailbox_flush.take()]
190            .into_iter()
191            .flatten()
192        {
193            task.abort();
194        }
195    }
196}