Skip to main content

kcode_k1_chat_state/
lib.rs

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