Skip to main content

kcode_k1_chat_chatend/
lib.rs

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;