Skip to main content

kcode_k1_chat_state/
lib.rs

1use std::sync::{Arc, atomic::AtomicU8};
2
3pub use kcode_k1_chat_chatend::{
4    BoxId, ChatBox, DispatchedToolCall, ProviderCall, RecoveryError, ToolCallId, TransitionError,
5};
6
7#[derive(Clone, Debug)]
8pub struct InferenceStart {
9    pub job: u64,
10    pub frontier: Option<BoxId>,
11    pub attempt: Arc<AtomicU8>,
12}
13
14#[derive(Debug)]
15pub enum StateError {
16    Transition(TransitionError),
17    Recovery(RecoveryError),
18    WrongInference { expected: Option<u64>, actual: u64 },
19    JobIdExhausted,
20    NotStalled,
21    Busy,
22}
23
24impl From<TransitionError> for StateError {
25    fn from(error: TransitionError) -> Self {
26        Self::Transition(error)
27    }
28}
29
30struct ActiveInference {
31    job: u64,
32    frontier: Option<BoxId>,
33    _attempt: Arc<AtomicU8>,
34}
35
36struct RetryRound {
37    frontier: Option<BoxId>,
38    ready: bool,
39}
40
41pub struct ActorState {
42    chatend: kcode_k1_chat_chatend::Chatend,
43    next_job: u64,
44    active: Option<ActiveInference>,
45    retry: Option<RetryRound>,
46    scheduled: bool,
47    arrival_during_active: bool,
48    halt: Option<String>,
49}
50
51impl ActorState {
52    pub fn new(force: bool) -> Self {
53        Self {
54            chatend: kcode_k1_chat_chatend::Chatend::new(),
55            next_job: 0,
56            active: None,
57            retry: None,
58            scheduled: force,
59            arrival_during_active: false,
60            halt: None,
61        }
62    }
63
64    pub fn recover(boxes: Vec<ChatBox>, force: bool) -> Result<Self, StateError> {
65        Ok(Self {
66            chatend: kcode_k1_chat_chatend::Chatend::recover(boxes)
67                .map_err(StateError::Recovery)?,
68            next_job: 0,
69            active: None,
70            retry: None,
71            scheduled: force,
72            arrival_during_active: false,
73            halt: None,
74        })
75    }
76
77    pub fn boxes(&self) -> &[ChatBox] {
78        self.chatend.boxes()
79    }
80
81    pub fn halted(&self) -> bool {
82        self.halt.is_some() || self.retry.is_some()
83    }
84
85    pub fn halt(&mut self, text: String) -> bool {
86        if self.halted() {
87            false
88        } else {
89            self.halt = Some(text);
90            true
91        }
92    }
93
94    pub fn take_halt(&mut self) -> Option<String> {
95        self.halt.take()
96    }
97
98    pub fn restart(&mut self) -> Result<(), StateError> {
99        if !self.halted() {
100            return Err(StateError::NotStalled);
101        }
102        if self.active.is_some() {
103            return Err(StateError::Busy);
104        }
105        self.halt = None;
106        if let Some(retry) = &mut self.retry {
107            retry.ready = true;
108        } else {
109            self.scheduled = true;
110        }
111        Ok(())
112    }
113
114    pub fn accept_box(
115        &mut self,
116        box_type: String,
117        contents: String,
118        hidden_type: String,
119        hidden_contents: String,
120    ) -> Result<(), StateError> {
121        self.chatend
122            .accept_box(box_type, contents, hidden_type, hidden_contents)?;
123        self.arrival();
124        Ok(())
125    }
126
127    pub fn accept_system(&mut self, contents: String) -> Result<(), StateError> {
128        self.chatend.accept_system(contents)?;
129        self.arrival();
130        Ok(())
131    }
132
133    pub fn accept_user(&mut self, contents: String) -> Result<(), StateError> {
134        self.chatend.accept_user(contents)?;
135        self.arrival();
136        Ok(())
137    }
138
139    pub fn accept_attachment(
140        &mut self,
141        contents: String,
142        hidden_type: String,
143        hidden_contents: String,
144    ) -> Result<(), StateError> {
145        self.chatend
146            .accept_attachment(contents, hidden_type, hidden_contents)?;
147        self.arrival();
148        Ok(())
149    }
150
151    pub fn accept_async_return(
152        &mut self,
153        tool_call_id: ToolCallId,
154        result: Result<String, String>,
155    ) -> Result<(), StateError> {
156        self.chatend.accept_async_return(tool_call_id, result)?;
157        self.arrival();
158        Ok(())
159    }
160
161    pub fn force_inference(&mut self) {
162        self.scheduled = true;
163    }
164
165    pub fn begin_inference(&mut self) -> Result<Option<InferenceStart>, StateError> {
166        if self.halt.is_some() || self.active.is_some() {
167            return Ok(None);
168        }
169        if let Some(retry) = &self.retry {
170            if !retry.ready {
171                return Ok(None);
172            }
173            let frontier = retry.frontier;
174            let start = self.activate(frontier)?;
175            self.retry = None;
176            return Ok(Some(start));
177        }
178        if !self.scheduled {
179            return Ok(None);
180        }
181        let job = self.next_job()?;
182        self.chatend.start_round()?;
183        let frontier = self.boxes().last().map(ChatBox::id);
184        self.next_job = job;
185        self.scheduled = false;
186        Ok(Some(self.install_active(job, frontier)))
187    }
188
189    pub fn append_stage(
190        &mut self,
191        job: u64,
192        contents: String,
193        calls: Vec<ProviderCall>,
194    ) -> Result<Vec<DispatchedToolCall>, StateError> {
195        self.require_job(job)?;
196        Ok(self.chatend.append_stage(contents, calls)?)
197    }
198
199    pub fn flush_active_arrivals(&mut self, job: u64) -> Result<Vec<ChatBox>, StateError> {
200        self.require_job(job)?;
201        let arrivals = self.chatend.flush_active_arrivals()?;
202        if !arrivals.is_empty() {
203            self.arrival_during_active = false;
204        }
205        Ok(arrivals)
206    }
207
208    pub fn complete_inference(&mut self, job: u64, contents: String) -> Result<(), StateError> {
209        self.require_job(job)?;
210        self.chatend.done(contents)?;
211        self.active = None;
212        if self.arrival_during_active {
213            self.scheduled = true;
214            self.arrival_during_active = false;
215        }
216        Ok(())
217    }
218
219    pub fn stall_inference(&mut self, job: u64, text: String) -> Result<(), StateError> {
220        self.require_job(job)?;
221        let active = self.active.take().expect("validated active inference");
222        self.halt = Some(text);
223        self.retry = Some(RetryRound {
224            frontier: active.frontier,
225            ready: false,
226        });
227        Ok(())
228    }
229
230    pub fn quiet(&self) -> bool {
231        self.active.is_none() && self.retry.is_none() && !self.scheduled
232    }
233
234    fn arrival(&mut self) {
235        if self.active.is_some() || self.retry.is_some() {
236            self.arrival_during_active = true;
237        } else {
238            self.scheduled = true;
239        }
240    }
241
242    fn activate(&mut self, frontier: Option<BoxId>) -> Result<InferenceStart, StateError> {
243        let job = self.next_job()?;
244        self.next_job = job;
245        Ok(self.install_active(job, frontier))
246    }
247
248    fn install_active(&mut self, job: u64, frontier: Option<BoxId>) -> InferenceStart {
249        let attempt = Arc::new(AtomicU8::new(1));
250        self.active = Some(ActiveInference {
251            job,
252            frontier,
253            _attempt: attempt.clone(),
254        });
255        InferenceStart {
256            job,
257            frontier,
258            attempt,
259        }
260    }
261
262    fn next_job(&self) -> Result<u64, StateError> {
263        self.next_job
264            .checked_add(1)
265            .ok_or(StateError::JobIdExhausted)
266    }
267
268    fn require_job(&self, job: u64) -> Result<(), StateError> {
269        let expected = self.active.as_ref().map(|active| active.job);
270        if expected == Some(job) {
271            Ok(())
272        } else {
273            Err(StateError::WrongInference {
274                expected,
275                actual: job,
276            })
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests;