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 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}