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