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