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;