1pub use kcode_k1_chat_core::ActionId;
2use std::collections::{BTreeMap, BTreeSet};
3
4#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
5pub struct BoxId(u64);
6
7impl BoxId {
8 pub fn get(self) -> u64 {
9 self.0
10 }
11}
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum BoxContent {
15 System(String),
16 User(String),
17 Kennedy {
18 text: String,
19 complete: bool,
20 },
21 Attachment,
22 KtoolCall {
23 action_id: ActionId,
24 name: String,
25 arguments: String,
26 },
27 KtoolReturn {
28 action_id: ActionId,
29 originating_call: BoxId,
30 result: Result<String, String>,
31 },
32}
33
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct ChatBox {
36 id: BoxId,
37 content: BoxContent,
38}
39
40impl ChatBox {
41 pub fn id(&self) -> BoxId {
42 self.id
43 }
44
45 pub fn content(&self) -> &BoxContent {
46 &self.content
47 }
48}
49
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct DispatchCall {
52 pub action_id: ActionId,
53 pub call_box_id: BoxId,
54 pub name: String,
55 pub arguments: String,
56}
57
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub enum DispatchOutcome {
60 Pending,
61 Terminal(Result<String, String>),
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub enum TransitionError {
66 InvalidPhase,
67 ActionCount,
68 OutcomeCount,
69 DuplicateAction,
70 UnknownAction,
71 DuplicateReturn,
72 BoxIdOverflow,
73}
74
75pub struct Chatend {
76 boxes: Vec<ChatBox>,
77 phase: Phase,
78 queued: Vec<BoxContent>,
79 actions: BTreeMap<ActionId, ActionRecord>,
80 last_id: u64,
81}
82
83enum Phase {
84 Idle,
85 Provider(Vec<StagedCall>),
86 Dispatch(Vec<DispatchCall>),
87}
88
89#[derive(Clone)]
90struct StagedCall {
91 name: String,
92 arguments: String,
93}
94
95struct ActionRecord {
96 call_box_id: BoxId,
97 returned: bool,
98}
99
100impl Default for Chatend {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl Chatend {
107 pub fn new() -> Self {
108 Self {
109 boxes: Vec::new(),
110 phase: Phase::Idle,
111 queued: Vec::new(),
112 actions: BTreeMap::new(),
113 last_id: 0,
114 }
115 }
116
117 pub fn boxes(&self) -> &[ChatBox] {
118 &self.boxes
119 }
120
121 pub fn accept_system(&mut self, text: String) -> Result<Option<BoxId>, TransitionError> {
122 self.accept_arrival(BoxContent::System(text))
123 }
124
125 pub fn accept_user(&mut self, text: String) -> Result<Option<BoxId>, TransitionError> {
126 self.accept_arrival(BoxContent::User(text))
127 }
128
129 pub fn accept_attachment(&mut self) -> Result<Option<BoxId>, TransitionError> {
130 self.accept_arrival(BoxContent::Attachment)
131 }
132
133 pub fn accept_async_return(
134 &mut self,
135 action_id: ActionId,
136 result: Result<String, String>,
137 ) -> Result<Option<BoxId>, TransitionError> {
138 let record = self
139 .actions
140 .get(&action_id)
141 .ok_or(TransitionError::UnknownAction)?;
142 if record.returned {
143 return Err(TransitionError::DuplicateReturn);
144 }
145 let originating_call = record.call_box_id;
146 let idle = matches!(&self.phase, Phase::Idle);
147 if idle {
148 self.ensure_capacity(1)?;
149 }
150 self.actions.get_mut(&action_id).unwrap().returned = true;
151 let content = BoxContent::KtoolReturn {
152 action_id,
153 originating_call,
154 result,
155 };
156 if idle {
157 Ok(Some(self.append(content)?))
158 } else {
159 self.queued.push(content);
160 Ok(None)
161 }
162 }
163
164 pub fn start_round(&mut self) -> Result<Option<BoxId>, TransitionError> {
165 if !matches!(&self.phase, Phase::Idle) {
166 return Err(TransitionError::InvalidPhase);
167 }
168 let frontier = self.boxes.last().map(ChatBox::id);
169 self.ensure_capacity(1)?;
170 self.append(BoxContent::Kennedy {
171 text: String::new(),
172 complete: false,
173 })?;
174 self.phase = Phase::Provider(Vec::new());
175 Ok(frontier)
176 }
177
178 pub fn append_kennedy_text(&mut self, chunk: &str) -> Result<(), TransitionError> {
179 if !matches!(&self.phase, Phase::Provider(_)) {
180 return Err(TransitionError::InvalidPhase);
181 }
182 let BoxContent::Kennedy { text, .. } = &mut self.boxes.last_mut().unwrap().content else {
183 unreachable!()
184 };
185 text.push_str(chunk);
186 Ok(())
187 }
188
189 pub fn collect_provider_call(
190 &mut self,
191 name: String,
192 arguments: String,
193 ) -> Result<(), TransitionError> {
194 match &mut self.phase {
195 Phase::Provider(calls) => {
196 calls.push(StagedCall { name, arguments });
197 Ok(())
198 }
199 _ => Err(TransitionError::InvalidPhase),
200 }
201 }
202
203 pub fn complete_provider_output(
204 &mut self,
205 action_ids: &[ActionId],
206 ) -> Result<Vec<DispatchCall>, TransitionError> {
207 let staged = match &self.phase {
208 Phase::Provider(calls) => calls.clone(),
209 _ => return Err(TransitionError::InvalidPhase),
210 };
211 if action_ids.len() != staged.len() {
212 return Err(TransitionError::ActionCount);
213 }
214 let mut seen = BTreeSet::new();
215 for action_id in action_ids {
216 if !seen.insert(*action_id) || self.actions.contains_key(action_id) {
217 return Err(TransitionError::DuplicateAction);
218 }
219 }
220 let queued_growth = if staged.is_empty() {
221 self.queued.len()
222 } else {
223 0
224 };
225 let growth = staged
226 .len()
227 .checked_add(queued_growth)
228 .ok_or(TransitionError::BoxIdOverflow)?;
229 self.ensure_capacity(growth)?;
230 let BoxContent::Kennedy { complete, .. } = &mut self.boxes.last_mut().unwrap().content
231 else {
232 unreachable!()
233 };
234 *complete = true;
235 let mut calls = Vec::with_capacity(staged.len());
236 for (staged, action_id) in staged.into_iter().zip(action_ids.iter().copied()) {
237 let call_box_id = self.append(BoxContent::KtoolCall {
238 action_id,
239 name: staged.name.clone(),
240 arguments: staged.arguments.clone(),
241 })?;
242 let record = ActionRecord {
243 call_box_id,
244 returned: false,
245 };
246 self.actions.insert(action_id, record);
247 calls.push(DispatchCall {
248 action_id,
249 call_box_id,
250 name: staged.name,
251 arguments: staged.arguments,
252 });
253 }
254 if calls.is_empty() {
255 self.phase = Phase::Idle;
256 self.drain_queued()?;
257 } else {
258 self.phase = Phase::Dispatch(calls.clone());
259 }
260 Ok(calls)
261 }
262
263 pub fn complete_dispatch(
264 &mut self,
265 outcomes: Vec<DispatchOutcome>,
266 ) -> Result<(), TransitionError> {
267 let calls = match &self.phase {
268 Phase::Dispatch(calls) => calls.clone(),
269 _ => return Err(TransitionError::InvalidPhase),
270 };
271 if outcomes.len() != calls.len() {
272 return Err(TransitionError::OutcomeCount);
273 }
274 for (call, outcome) in calls.iter().zip(&outcomes) {
275 let record = self.actions.get(&call.action_id).unwrap();
276 if matches!(outcome, DispatchOutcome::Terminal(_)) && record.returned {
277 return Err(TransitionError::DuplicateReturn);
278 }
279 }
280 let terminals = outcomes
281 .iter()
282 .filter(|outcome| matches!(outcome, DispatchOutcome::Terminal(_)))
283 .count();
284 let growth = terminals
285 .checked_add(self.queued.len())
286 .ok_or(TransitionError::BoxIdOverflow)?;
287 self.ensure_capacity(growth)?;
288 for (call, outcome) in calls.into_iter().zip(outcomes) {
289 if let DispatchOutcome::Terminal(result) = outcome {
290 let originating_call = {
291 let record = self.actions.get_mut(&call.action_id).unwrap();
292 record.returned = true;
293 record.call_box_id
294 };
295 self.append(BoxContent::KtoolReturn {
296 action_id: call.action_id,
297 originating_call,
298 result,
299 })?;
300 }
301 }
302 self.phase = Phase::Idle;
303 self.drain_queued()
304 }
305
306 fn accept_arrival(&mut self, content: BoxContent) -> Result<Option<BoxId>, TransitionError> {
307 if matches!(&self.phase, Phase::Idle) {
308 self.ensure_capacity(1)?;
309 Ok(Some(self.append(content)?))
310 } else {
311 self.queued.push(content);
312 Ok(None)
313 }
314 }
315
316 fn drain_queued(&mut self) -> Result<(), TransitionError> {
317 let queued = std::mem::take(&mut self.queued);
318 for content in queued {
319 self.append(content)?;
320 }
321 Ok(())
322 }
323
324 fn ensure_capacity(&self, additional: usize) -> Result<(), TransitionError> {
325 let additional = u64::try_from(additional).map_err(|_| TransitionError::BoxIdOverflow)?;
326 self.last_id
327 .checked_add(additional)
328 .ok_or(TransitionError::BoxIdOverflow)?;
329 Ok(())
330 }
331
332 fn append(&mut self, content: BoxContent) -> Result<BoxId, TransitionError> {
333 self.ensure_capacity(1)?;
334 self.last_id += 1;
335 let id = BoxId(self.last_id);
336 self.boxes.push(ChatBox { id, content });
337 Ok(id)
338 }
339}
340
341#[cfg(test)]
342mod tests;