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