kcode_k1_chat_codex_codec/
lib.rs1#![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)]
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}