kcode_k1_codex_conversation_values/
lib.rs1use serde_json::Value;
2use std::{
3 collections::HashSet,
4 fmt,
5 path::PathBuf,
6 sync::{Arc, Mutex, MutexGuard},
7};
8
9#[derive(Clone, Debug)]
10pub struct Config {
11 pub executable: PathBuf,
12 pub working_directory: String,
13 pub model: String,
14 pub reasoning_effort: Option<String>,
15 pub base_instructions: String,
16 pub tools: Vec<DynamicTool>,
17}
18
19impl Config {
20 pub fn validate(&self) -> Result<(), Error> {
21 validate_config(self)
22 }
23}
24
25#[derive(Clone, Debug, PartialEq)]
26pub struct DynamicTool {
27 pub name: String,
28 pub description: String,
29 pub input_schema: Value,
30}
31
32#[derive(Clone, Debug, PartialEq)]
33pub struct ToolCall {
34 pub call_id: String,
35 pub name: String,
36 pub arguments: Value,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct ToolResult {
41 pub success: bool,
42 pub output: String,
43}
44
45#[derive(Clone, Debug, PartialEq)]
46pub enum Event {
47 TextDelta(String),
48 ToolCall(ToolCall),
49 Done,
50 Error(Error),
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum ErrorKind {
55 Busy,
56 Interrupted,
57 InvalidToolResult,
58 LaunchRejected,
59 Protocol,
60 Server,
61 Unavailable,
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct Error {
66 pub kind: ErrorKind,
67 pub message: String,
68 pub diagnostics: Vec<u8>,
69}
70
71impl Error {
72 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
73 Self {
74 kind,
75 message: message.into(),
76 diagnostics: Vec::new(),
77 }
78 }
79
80 pub fn with_diagnostics(mut self, diagnostics: Vec<u8>) -> Self {
81 self.diagnostics = diagnostics;
82 self
83 }
84
85 pub fn server(value: &Value, diagnostics: &Diagnostics) -> Self {
86 let detail = value
87 .get("message")
88 .and_then(Value::as_str)
89 .or_else(|| value.pointer("/error/message").and_then(Value::as_str));
90 diagnostics.error(
91 ErrorKind::Server,
92 detail.map_or_else(
93 || "Codex app-server error".to_owned(),
94 |text| format!("Codex app-server error: {text}"),
95 ),
96 )
97 }
98}
99
100impl fmt::Display for Error {
101 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102 formatter.write_str(&self.message)
103 }
104}
105
106impl std::error::Error for Error {}
107
108#[derive(Clone, Debug, Default)]
109pub struct Diagnostics(Arc<Mutex<Vec<u8>>>);
110
111impl Diagnostics {
112 pub fn new(bytes: Vec<u8>) -> Self {
113 Self(Arc::new(Mutex::new(bytes)))
114 }
115
116 fn lock(&self) -> MutexGuard<'_, Vec<u8>> {
117 self.0
118 .lock()
119 .unwrap_or_else(|poisoned| poisoned.into_inner())
120 }
121
122 pub fn snapshot(&self) -> Vec<u8> {
123 self.lock().clone()
124 }
125
126 pub fn replace(&self, bytes: Vec<u8>) {
127 *self.lock() = bytes;
128 }
129
130 pub fn error(&self, kind: ErrorKind, message: impl Into<String>) -> Error {
131 Error {
132 kind,
133 message: message.into(),
134 diagnostics: self.snapshot(),
135 }
136 }
137}
138
139fn ensure(valid: bool, error: Error) -> Result<(), Error> {
140 valid.then_some(()).ok_or(error)
141}
142
143fn protocol(message: impl Into<String>) -> Error {
144 Error::new(ErrorKind::Protocol, message)
145}
146
147pub fn validate_config(config: &Config) -> Result<(), Error> {
148 let mut names = HashSet::new();
149 for tool in &config.tools {
150 ensure(
151 !tool.name.is_empty(),
152 protocol("dynamic tool names must not be empty"),
153 )?;
154 ensure(
155 names.insert(&tool.name),
156 protocol(format!("duplicate dynamic tool name: {}", tool.name)),
157 )?;
158 }
159 Ok(())
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use serde_json::json;
166
167 fn tool(name: &str) -> DynamicTool {
168 DynamicTool {
169 name: name.to_owned(),
170 description: format!("Use {name}"),
171 input_schema: json!({"type": "object"}),
172 }
173 }
174
175 fn config(tools: Vec<DynamicTool>) -> Config {
176 Config {
177 executable: "codex".into(),
178 working_directory: "/work".into(),
179 model: "model".into(),
180 reasoning_effort: Some("high".into()),
181 base_instructions: "instructions".into(),
182 tools,
183 }
184 }
185
186 #[test]
187 fn valid_config_is_accepted() {
188 assert!(
189 config(vec![tool("search"), tool("read")])
190 .validate()
191 .is_ok()
192 );
193 assert!(validate_config(&config(Vec::new())).is_ok());
194 }
195
196 #[test]
197 fn empty_dynamic_tool_name_is_rejected() {
198 assert_eq!(
199 config(vec![tool("")]).validate().unwrap_err(),
200 Error::new(ErrorKind::Protocol, "dynamic tool names must not be empty")
201 );
202 }
203
204 #[test]
205 fn duplicate_dynamic_tool_name_is_rejected() {
206 assert_eq!(
207 config(vec![tool("search"), tool("search")])
208 .validate()
209 .unwrap_err(),
210 Error::new(ErrorKind::Protocol, "duplicate dynamic tool name: search")
211 );
212 }
213
214 #[test]
215 fn diagnostics_snapshot_replace_and_stamp_errors() {
216 let diagnostics = Diagnostics::new(vec![1, 2]);
217 let observer = diagnostics.clone();
218 assert_eq!(diagnostics.snapshot(), vec![1, 2]);
219 diagnostics.replace(vec![3, 4]);
220 assert_eq!(observer.snapshot(), vec![3, 4]);
221 assert_eq!(
222 diagnostics.error(ErrorKind::Unavailable, "offline"),
223 Error {
224 kind: ErrorKind::Unavailable,
225 message: "offline".into(),
226 diagnostics: vec![3, 4],
227 }
228 );
229 }
230
231 #[test]
232 fn with_diagnostics_preserves_error_and_display_behavior() {
233 let error = Error::new(ErrorKind::Interrupted, "stopped").with_diagnostics(vec![5, 6]);
234 assert_eq!(
235 error,
236 Error {
237 kind: ErrorKind::Interrupted,
238 message: "stopped".into(),
239 diagnostics: vec![5, 6],
240 }
241 );
242 assert_eq!(error.to_string(), "stopped");
243 let _: &(dyn std::error::Error + 'static) = &error;
244 }
245
246 #[test]
247 fn server_error_extracts_direct_nested_and_fallback_messages() {
248 let diagnostics = Diagnostics::new(b"stderr".to_vec());
249 for (value, message) in [
250 (
251 json!({"message": "direct", "error": {"message": "nested"}}),
252 "Codex app-server error: direct",
253 ),
254 (
255 json!({"error": {"message": "nested"}}),
256 "Codex app-server error: nested",
257 ),
258 (json!({"error": {"message": 7}}), "Codex app-server error"),
259 ] {
260 assert_eq!(
261 Error::server(&value, &diagnostics),
262 Error {
263 kind: ErrorKind::Server,
264 message: message.into(),
265 diagnostics: b"stderr".to_vec(),
266 }
267 );
268 }
269 }
270}