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