1#![forbid(unsafe_code)]
2
3use kcode_k1_access_kmap::K1AccessKmap;
4use kcode_k1_chat_persistence::Session;
5pub use kcode_k1_chat_state::BoxId;
6use kcode_k1_chat_state::{AGENT_MESSAGE_TYPE, AGENT_RESPONSE_TYPE, USER_MESSAGE_TYPE};
7pub use kcode_k1_chat_thread_durable_turn::{
8 BoxValue, ChatBox, EventRecord, ModelUsage, PreparedCall, PreparedMailboxFlush, Status,
9 TokenBreakdown, ToolCallId,
10};
11use kcode_k1_chat_thread_durable_turn::{DurableTurn, RestartError, ShimOutput};
12use kcode_k1_chat_thread_ktools::ChatThreadKtools;
13pub use kcode_k1_chat_thread_ktools::{AccessContext, AccessPolicy, ProfileId, SetLaunchNodeKtool};
14use serde::Deserialize;
15use std::sync::Arc;
16
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub enum TransitionError {
19 Unauthorized,
20 NotStalled,
21 NotRestartable,
22 Internal(String),
23}
24
25pub struct DurableThread {
26 turn: DurableTurn,
27 ktools: ChatThreadKtools,
28 authorized: bool,
29}
30
31impl DurableThread {
32 pub fn recover(session: Session, kmap: Arc<K1AccessKmap>) -> Result<Self, String> {
33 Self::recover_with_ktools(session, ChatThreadKtools::new(kmap))
34 }
35
36 pub fn recover_with_social(
37 session: Session,
38 kmap: Arc<K1AccessKmap>,
39 social: kcode_k1_ktool_social::SocialKtools,
40 ) -> Result<Self, String> {
41 Self::recover_with_ktools(session, ChatThreadKtools::new_with_social(kmap, social))
42 }
43
44 pub fn recover_with_social_and_set_launch_node(
45 session: Session,
46 kmap: Arc<K1AccessKmap>,
47 social: kcode_k1_ktool_social::SocialKtools,
48 set_launch_node: SetLaunchNodeKtool,
49 ) -> Result<Self, String> {
50 Self::recover_with_ktools(
51 session,
52 ChatThreadKtools::new_with_social_and_set_launch_node(kmap, social, set_launch_node),
53 )
54 }
55
56 pub fn boxes(&self) -> &[ChatBox] {
57 self.turn.boxes()
58 }
59
60 pub fn events(&self) -> Vec<EventRecord> {
61 self.turn.events()
62 }
63
64 pub fn status(&self) -> Status {
65 self.turn.status()
66 }
67
68 pub fn accept_box(
69 &mut self,
70 box_type: String,
71 contents: String,
72 hidden_type: String,
73 hidden_contents: String,
74 ) -> Result<(), String> {
75 self.turn
76 .accept(box_type, contents, hidden_type, hidden_contents)
77 }
78
79 pub fn accept_external_box(
80 &mut self,
81 box_type: String,
82 contents: String,
83 hidden_type: String,
84 hidden_contents: String,
85 ) -> Result<(), TransitionError> {
86 if box_type == USER_MESSAGE_TYPE {
87 return Err(TransitionError::Unauthorized);
88 }
89 self.accept_box(box_type, contents, hidden_type, hidden_contents)
90 .map_err(TransitionError::Internal)
91 }
92
93 pub fn accept_user(
94 &mut self,
95 context: AccessContext,
96 profile_id: ProfileId,
97 policy: AccessPolicy,
98 contents: String,
99 ) -> Result<(), TransitionError> {
100 let installed = self.bind_authorization(context, profile_id, policy)?;
101 match self.turn.accept(
102 USER_MESSAGE_TYPE.into(),
103 contents,
104 String::new(),
105 String::new(),
106 ) {
107 Ok(()) => Ok(()),
108 Err(error) => {
109 if installed {
110 self.clear_authorization();
111 }
112 Err(TransitionError::Internal(error))
113 }
114 }
115 }
116
117 pub fn accept_return(
118 &mut self,
119 id: ToolCallId,
120 result: Result<String, String>,
121 ) -> Result<(), String> {
122 self.turn.accept_tool_return(id, result)
123 }
124
125 pub fn prepare_stage(
126 &mut self,
127 job: u64,
128 text: String,
129 boxes: Vec<BoxValue>,
130 ) -> Result<Vec<PreparedCall>, String> {
131 self.turn.prepare_stage(job, text, boxes)
132 }
133
134 pub fn launch_action(&mut self, name: &str, arguments: &str) -> Result<String, String> {
135 if !kcode_k1_ktool_docs::is_known_ktool(name) {
136 return Err("unknown Ktool".into());
137 }
138 match name {
139 "KtoolDocs" => kcode_k1_ktool_docs::ktool_docs(arguments),
140 "SendMessage" => launch_send_message(&mut self.turn, arguments),
141 _ => self.ktools.launch(name, arguments),
142 }
143 }
144
145 pub fn accept_tool_message(&mut self, id: ToolCallId, contents: String) -> Result<(), String> {
146 self.turn.accept_tool_message(id, contents)
147 }
148
149 pub fn accept_tool_return(
150 &mut self,
151 id: ToolCallId,
152 result: Result<String, String>,
153 ) -> Result<(), String> {
154 self.turn.accept_tool_return(id, result)
155 }
156
157 pub fn accept_tool_return_v2(
158 &mut self,
159 id: ToolCallId,
160 result: Result<String, String>,
161 metadata_type: String,
162 metadata_contents: String,
163 ) -> Result<(), String> {
164 self.turn
165 .accept_tool_return_v2(id, result, metadata_type, metadata_contents)
166 }
167
168 pub fn prepare_mailbox_flush(
169 &mut self,
170 job: u64,
171 ) -> Result<Option<PreparedMailboxFlush>, String> {
172 self.turn.prepare_mailbox_flush(job)
173 }
174
175 pub fn prepared_input(&self, prepared: &PreparedMailboxFlush) -> Result<String, String> {
176 self.turn.validate_mailbox_flush(prepared)?;
177 render_input(prepared.values())
178 }
179
180 pub fn commit_mailbox_flush(&mut self, prepared: PreparedMailboxFlush) -> Result<(), String> {
181 self.turn.commit_mailbox_flush(prepared)
182 }
183
184 pub fn begin_input(&mut self) -> Result<Option<(u64, String)>, String> {
185 let Some(start) = self.turn.begin()? else {
186 return Ok(None);
187 };
188 Ok(Some((start.job, render_input(&start.values)?)))
189 }
190
191 pub fn complete(&mut self, job: u64, output: ShimOutput<BoxValue>) -> Result<bool, String> {
192 self.turn.complete(job, output)
193 }
194
195 pub fn complete_with_terminal_response(
196 &mut self,
197 job: u64,
198 output: ShimOutput<BoxValue>,
199 ) -> Result<(bool, u64), String> {
200 let terminal_index = self.turn.boxes().len();
201 let resume = self.complete(job, output)?;
202 let terminal =
203 self.turn.boxes().get(terminal_index).ok_or_else(|| {
204 "completion did not append a terminal Agent Response box".to_owned()
205 })?;
206 if terminal.box_type() != AGENT_RESPONSE_TYPE {
207 return Err("completion terminal box was not an Agent Response".to_owned());
208 }
209 Ok((resume, terminal.id().get()))
210 }
211
212 pub fn record_model_usage(
213 &mut self,
214 connected_box_id: u64,
215 usage: ModelUsage,
216 ) -> Result<(), String> {
217 self.turn.record_model_usage(connected_box_id, usage)
218 }
219
220 pub fn fail(&mut self, job: u64, error: String, restartable: bool) {
221 self.turn.fail(job, error, restartable);
222 self.clear_authorization();
223 }
224
225 pub fn restart(
226 &mut self,
227 context: AccessContext,
228 profile_id: ProfileId,
229 policy: AccessPolicy,
230 ) -> Result<(), TransitionError> {
231 let installed = self.bind_authorization(context, profile_id, policy)?;
232 if let Err(error) = self.turn.restart().map_err(|error| match error {
233 RestartError::NotStalled => TransitionError::NotStalled,
234 RestartError::ProviderActionAccepted => TransitionError::NotRestartable,
235 }) {
236 if installed {
237 self.clear_authorization();
238 }
239 return Err(error);
240 }
241 Ok(())
242 }
243
244 pub fn clear_authorization(&mut self) {
245 self.ktools.clear_authorization();
246 self.authorized = false;
247 }
248
249 fn recover_with_ktools(session: Session, ktools: ChatThreadKtools) -> Result<Self, String> {
250 Ok(Self {
251 turn: DurableTurn::recover(session)?,
252 ktools,
253 authorized: false,
254 })
255 }
256
257 fn bind_authorization(
258 &mut self,
259 context: AccessContext,
260 profile_id: ProfileId,
261 policy: AccessPolicy,
262 ) -> Result<bool, TransitionError> {
263 let installed = !self.authorized;
264 if self
265 .ktools
266 .bind_authorization(context, profile_id, policy)
267 .is_err()
268 {
269 if installed {
270 self.ktools.clear_authorization();
271 }
272 return Err(TransitionError::Unauthorized);
273 }
274 self.authorized = true;
275 Ok(installed)
276 }
277}
278
279#[derive(Deserialize)]
280#[serde(deny_unknown_fields)]
281struct SendMessageArguments {
282 message: String,
283}
284
285fn launch_send_message(turn: &mut DurableTurn, arguments: &str) -> Result<String, String> {
286 let parsed: SendMessageArguments =
287 serde_json::from_str(arguments).map_err(|_| invalid_send_message())?;
288 if parsed.message.is_empty() {
289 return Err(invalid_send_message());
290 }
291 turn.accept(
292 AGENT_MESSAGE_TYPE.into(),
293 parsed.message,
294 String::new(),
295 String::new(),
296 )?;
297 Ok("success".into())
298}
299
300fn invalid_send_message() -> String {
301 "invalid SendMessage arguments".into()
302}
303
304fn render_input(values: &[BoxValue]) -> Result<String, String> {
305 let mut output = String::new();
306 for value in values {
307 let BoxValue::History(section) = value else {
308 return Err("Codex provider input contains a non-history value".into());
309 };
310 if section.is_empty() {
311 continue;
312 }
313 if !output.is_empty() && !output.ends_with('\n') {
314 output.push('\n');
315 }
316 output.push_str(section);
317 }
318 Ok(output)
319}
320
321#[cfg(test)]
322mod tests;