Skip to main content

kcode_k1_codex_conversation_values/
lib.rs

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