Skip to main content

kcode_k1_chat_codex_codec/
lib.rs

1#![forbid(unsafe_code)]
2
3use kcode_k1_chat_chatend::{AGENT_RESPONSE_TYPE, BoxId, ChatBox};
4use serde_json::Value;
5
6#[derive(Clone, Debug, Eq, PartialEq)]
7pub struct Call {
8    pub name: String,
9    pub arguments: String,
10}
11
12/// Preserved input for a malformed native action, for later recovery handling.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct MalformedNativeAction {
15    kind: &'static str,
16    native_tool: String,
17    attempted_ktool: Option<String>,
18    validation_code: &'static str,
19    path: &'static str,
20    expected: &'static str,
21    received: String,
22    native_arguments: Value,
23}
24
25impl MalformedNativeAction {
26    pub fn kind(&self) -> &str {
27        self.kind
28    }
29
30    pub fn native_tool(&self) -> &str {
31        &self.native_tool
32    }
33
34    pub fn attempted_ktool(&self) -> Option<&str> {
35        self.attempted_ktool.as_deref()
36    }
37
38    pub fn validation_code(&self) -> &str {
39        self.validation_code
40    }
41
42    pub fn path(&self) -> &str {
43        self.path
44    }
45
46    pub fn expected(&self) -> &str {
47        self.expected
48    }
49
50    pub fn received(&self) -> &str {
51        &self.received
52    }
53
54    pub fn native_arguments(&self) -> &Value {
55        &self.native_arguments
56    }
57
58    pub fn native_arguments_json(&self) -> String {
59        canonical_json(&self.native_arguments)
60    }
61
62    pub fn diagnostic(&self) -> String {
63        format!(
64            "malformed native action: kind={}; native_tool={}; attempted_ktool={}; validation_code={}; path={}; expected={}; received={}",
65            self.kind,
66            self.native_tool,
67            self.attempted_ktool.as_deref().unwrap_or("<unknown>"),
68            self.validation_code,
69            self.path,
70            self.expected,
71            self.received,
72        )
73    }
74
75    pub fn diagnostic_json(&self) -> String {
76        canonical_json(&serde_json::json!({
77            "kind": self.kind,
78            "native_tool": self.native_tool,
79            "attempted_ktool": self.attempted_ktool,
80            "validation_code": self.validation_code,
81            "path": self.path,
82            "expected": self.expected,
83            "received": self.received,
84            "native_arguments": self.native_arguments,
85        }))
86    }
87}
88
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub enum BoxValue {
91    History(String),
92    Call(Result<Call, String>),
93    AgentMessage(Result<String, String>),
94    MalformedNativeAction(MalformedNativeAction),
95}
96
97#[derive(Default)]
98pub struct Codec;
99
100impl kcode_k1_codex_adapter::BoxCodec for Codec {
101    type Box = BoxValue;
102
103    fn tool_call_box(&mut self, call: &kcode_k1_codex_adapter::ToolCall) -> Self::Box {
104        match call.name.as_str() {
105            "call_ktool" => self.call_ktool_box(call),
106            "agent_message" => self.agent_message_box(call),
107            _ => malformed(
108                "unknown_native_tool",
109                call,
110                None,
111                "unknown_native_tool",
112                "$.name",
113                "call_ktool or agent_message",
114                call.name.clone(),
115            ),
116        }
117    }
118
119    fn box_text<'a>(&self, box_: &'a Self::Box) -> &'a str {
120        match box_ {
121            BoxValue::History(text) => text,
122            BoxValue::Call(_) | BoxValue::AgentMessage(_) | BoxValue::MalformedNativeAction(_) => {
123                ""
124            }
125        }
126    }
127
128    fn malformed_tool_call_message(&self, box_: &Self::Box) -> Option<&'static str> {
129        match box_ {
130            BoxValue::Call(Err(error)) => Some(match error.as_str() {
131                "tool arguments must be an object" => "tool arguments must be an object",
132                "tool arguments must have name and arguments" => {
133                    "tool arguments must have name and arguments"
134                }
135                "tool arguments name must be a string" => "tool arguments name must be a string",
136                "tool arguments require arguments" => "tool arguments require arguments",
137                "tool arguments cannot be encoded" => "tool arguments cannot be encoded",
138                "tool name must be call_ktool or agent_message" => {
139                    "tool name must be call_ktool or agent_message"
140                }
141                _ => "call_ktool value is malformed",
142            }),
143            BoxValue::AgentMessage(Err(error)) => Some(match error.as_str() {
144                "agent_message arguments must be an object" => {
145                    "agent_message arguments must be an object"
146                }
147                "agent_message arguments must have exactly one message field" => {
148                    "agent_message arguments must have exactly one message field"
149                }
150                "agent_message message must be a string" => {
151                    "agent_message message must be a string"
152                }
153                "agent_message message must not be empty" => {
154                    "agent_message message must not be empty"
155                }
156                _ => "agent_message value is malformed",
157            }),
158            BoxValue::History(_)
159            | BoxValue::Call(Ok(_))
160            | BoxValue::AgentMessage(Ok(_))
161            | BoxValue::MalformedNativeAction(_) => None,
162        }
163    }
164}
165
166impl Codec {
167    fn call_ktool_box(&self, call: &kcode_k1_codex_adapter::ToolCall) -> BoxValue {
168        let Value::Object(values) = &call.arguments else {
169            return malformed(
170                "malformed_call_ktool",
171                call,
172                None,
173                "arguments_not_object",
174                "$",
175                "object with name and arguments",
176                value_kind(&call.arguments).into(),
177            );
178        };
179        let attempted_ktool = values
180            .get("name")
181            .and_then(Value::as_str)
182            .map(str::to_owned);
183        if values.len() != 2 || !values.contains_key("name") || !values.contains_key("arguments") {
184            return malformed(
185                "malformed_call_ktool",
186                call,
187                attempted_ktool,
188                "invalid_wrapper_fields",
189                "$",
190                "object with exactly name and arguments",
191                canonical_json(&call.arguments),
192            );
193        }
194        let Some(Value::String(name)) = values.get("name") else {
195            return malformed(
196                "malformed_call_ktool",
197                call,
198                None,
199                "name_not_string",
200                "$.name",
201                "string",
202                canonical_json(&values["name"]),
203            );
204        };
205        let arguments = &values["arguments"];
206        BoxValue::Call(Ok(Call {
207            name: name.clone(),
208            arguments: canonical_json(arguments),
209        }))
210    }
211
212    fn agent_message_box(&self, call: &kcode_k1_codex_adapter::ToolCall) -> BoxValue {
213        let Value::Object(values) = &call.arguments else {
214            return malformed(
215                "malformed_agent_message",
216                call,
217                None,
218                "arguments_not_object",
219                "$",
220                "object with exactly one message field",
221                value_kind(&call.arguments).into(),
222            );
223        };
224        if values.len() != 1 || !values.contains_key("message") {
225            return malformed(
226                "malformed_agent_message",
227                call,
228                None,
229                "invalid_message_fields",
230                "$",
231                "object with exactly one message field",
232                canonical_json(&call.arguments),
233            );
234        }
235        let Some(Value::String(message)) = values.get("message") else {
236            return malformed(
237                "malformed_agent_message",
238                call,
239                None,
240                "message_not_string",
241                "$.message",
242                "string",
243                canonical_json(&values["message"]),
244            );
245        };
246        if message.is_empty() {
247            return malformed(
248                "malformed_agent_message",
249                call,
250                None,
251                "message_empty",
252                "$.message",
253                "nonempty string",
254                "empty string".into(),
255            );
256        }
257        BoxValue::AgentMessage(Ok(message.clone()))
258    }
259}
260
261fn malformed(
262    kind: &'static str,
263    call: &kcode_k1_codex_adapter::ToolCall,
264    attempted_ktool: Option<String>,
265    validation_code: &'static str,
266    path: &'static str,
267    expected: &'static str,
268    received: String,
269) -> BoxValue {
270    BoxValue::MalformedNativeAction(MalformedNativeAction {
271        kind,
272        native_tool: call.name.clone(),
273        attempted_ktool,
274        validation_code,
275        path,
276        expected,
277        received,
278        native_arguments: call.arguments.clone(),
279    })
280}
281
282fn canonical_json(value: &Value) -> String {
283    match value {
284        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
285            serde_json::to_string(value).unwrap_or_default()
286        }
287        Value::Array(values) => format!(
288            "[{}]",
289            values
290                .iter()
291                .map(canonical_json)
292                .collect::<Vec<_>>()
293                .join(",")
294        ),
295        Value::Object(values) => {
296            let mut entries = values.iter().collect::<Vec<_>>();
297            entries.sort_unstable_by(|left, right| left.0.cmp(right.0));
298            format!(
299                "{{{}}}",
300                entries
301                    .into_iter()
302                    .map(|(key, value)| format!(
303                        "{}:{}",
304                        canonical_json(&Value::String(key.clone())),
305                        canonical_json(value)
306                    ))
307                    .collect::<Vec<_>>()
308                    .join(",")
309            )
310        }
311    }
312}
313
314fn value_kind(value: &Value) -> &'static str {
315    match value {
316        Value::Null => "null",
317        Value::Bool(_) => "boolean",
318        Value::Number(_) => "number",
319        Value::String(_) => "string",
320        Value::Array(_) => "array",
321        Value::Object(_) => "object",
322    }
323}
324
325pub fn project(box_: &ChatBox) -> BoxValue {
326    BoxValue::History(format!(
327        "[Box {} | {}]\n{}",
328        box_.id().get(),
329        box_.box_type(),
330        box_.contents()
331    ))
332}
333
334pub fn open_agent_response(id: BoxId) -> BoxValue {
335    BoxValue::History(format!("[Box {} | {}]\n", id.get(), AGENT_RESPONSE_TYPE))
336}