Skip to main content

kcode_k1_codex_adapter/
lib.rs

1mod runtime;
2mod shim;
3
4use serde_json::Value;
5use std::{
6    fmt,
7    path::PathBuf,
8    sync::{Arc, Mutex, Weak},
9};
10use tokio::sync::{mpsc, oneshot};
11
12pub use shim::{
13    ASYNC_TOOL_ACKNOWLEDGEMENT, BoxCodec, Shim, ShimItem, ShimOutput, ToolCallLauncher,
14    ToolLaunchFuture,
15};
16
17#[derive(Clone, Debug)]
18pub struct Config {
19    pub executable: PathBuf,
20    pub working_directory: String,
21    pub model: String,
22    pub reasoning_effort: Option<String>,
23    pub base_instructions: String,
24    pub tools: Vec<DynamicTool>,
25}
26
27#[derive(Clone, Debug, PartialEq)]
28pub struct DynamicTool {
29    pub name: String,
30    pub description: String,
31    pub input_schema: Value,
32}
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct ToolCall {
36    pub call_id: String,
37    pub name: String,
38    pub arguments: Value,
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct ToolResult {
43    pub success: bool,
44    pub output: String,
45}
46
47#[derive(Clone, Debug, PartialEq)]
48pub enum Event {
49    TextDelta(String),
50    ToolCall(ToolCall),
51    Done,
52    Error(Error),
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum ErrorKind {
57    Busy,
58    Interrupted,
59    InvalidToolResult,
60    LaunchRejected,
61    Protocol,
62    Server,
63    Unavailable,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct Error {
68    pub kind: ErrorKind,
69    pub message: String,
70    pub diagnostics: Vec<u8>,
71}
72
73impl fmt::Display for Error {
74    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75        formatter.write_str(&self.message)
76    }
77}
78
79impl std::error::Error for Error {}
80
81type Diagnostics = Arc<Mutex<Vec<u8>>>;
82type Reply = oneshot::Sender<Result<(), Error>>;
83type StartReply = oneshot::Sender<Result<u64, Error>>;
84type Events = mpsc::UnboundedSender<Event>;
85
86enum Command {
87    Start {
88        key: String,
89        input: String,
90        events: Events,
91        reply: StartReply,
92    },
93    ToolResult {
94        key: String,
95        turn: u64,
96        call_id: String,
97        result: ToolResult,
98        reply: Reply,
99    },
100    Abandon {
101        key: String,
102        turn: u64,
103    },
104}
105
106struct Client {
107    commands: mpsc::UnboundedSender<Command>,
108    diagnostics: Diagnostics,
109}
110
111#[derive(Clone)]
112pub struct Adapter {
113    client: Arc<Client>,
114}
115
116impl Adapter {
117    pub async fn open(config: Config) -> Result<Self, Error> {
118        runtime::open(config).await
119    }
120
121    pub async fn start_turn(
122        &self,
123        conversation_key: impl Into<String>,
124        input: impl Into<String>,
125    ) -> Result<Turn, Error> {
126        let key = conversation_key.into();
127        let (events, receiver) = mpsc::unbounded_channel();
128        let (reply, answer) = oneshot::channel();
129        self.client
130            .commands
131            .send(Command::Start {
132                key: key.clone(),
133                input: input.into(),
134                events,
135                reply,
136            })
137            .map_err(|_| self.unavailable())?;
138        let serial = answer.await.map_err(|_| self.unavailable())??;
139        Ok(Turn {
140            key,
141            serial,
142            client: Arc::downgrade(&self.client),
143            events: receiver,
144            diagnostics: self.client.diagnostics.clone(),
145            terminal: false,
146        })
147    }
148
149    pub fn diagnostics(&self) -> Vec<u8> {
150        snapshot(&self.client.diagnostics)
151    }
152
153    fn unavailable(&self) -> Error {
154        fault(
155            ErrorKind::Unavailable,
156            "Codex app-server is unavailable",
157            &self.client.diagnostics,
158        )
159    }
160}
161
162pub struct Turn {
163    key: String,
164    serial: u64,
165    client: Weak<Client>,
166    events: mpsc::UnboundedReceiver<Event>,
167    diagnostics: Diagnostics,
168    terminal: bool,
169}
170
171impl fmt::Debug for Turn {
172    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173        formatter
174            .debug_struct("Turn")
175            .field("key", &self.key)
176            .field("serial", &self.serial)
177            .finish_non_exhaustive()
178    }
179}
180
181impl Turn {
182    pub async fn next_event(&mut self) -> Option<Event> {
183        let event = self.events.recv().await;
184        if event
185            .as_ref()
186            .is_none_or(|event| matches!(event, Event::Done | Event::Error(_)))
187        {
188            self.terminal = true;
189        }
190        event
191    }
192
193    pub async fn respond(
194        &self,
195        call_id: impl Into<String>,
196        result: ToolResult,
197    ) -> Result<(), Error> {
198        let client = self.client.upgrade().ok_or_else(|| {
199            fault(
200                ErrorKind::Unavailable,
201                "Codex app-server is unavailable",
202                &self.diagnostics,
203            )
204        })?;
205        let (reply, answer) = oneshot::channel();
206        client
207            .commands
208            .send(Command::ToolResult {
209                key: self.key.clone(),
210                turn: self.serial,
211                call_id: call_id.into(),
212                result,
213                reply,
214            })
215            .map_err(|_| {
216                fault(
217                    ErrorKind::Unavailable,
218                    "Codex app-server is unavailable",
219                    &self.diagnostics,
220                )
221            })?;
222        answer.await.map_err(|_| {
223            fault(
224                ErrorKind::Unavailable,
225                "Codex app-server is unavailable",
226                &self.diagnostics,
227            )
228        })?
229    }
230}
231
232impl Drop for Turn {
233    fn drop(&mut self) {
234        if !self.terminal
235            && let Some(client) = self.client.upgrade()
236        {
237            let _ = client.commands.send(Command::Abandon {
238                key: self.key.clone(),
239                turn: self.serial,
240            });
241        }
242    }
243}
244
245fn snapshot(diagnostics: &Diagnostics) -> Vec<u8> {
246    diagnostics
247        .lock()
248        .unwrap_or_else(|poisoned| poisoned.into_inner())
249        .clone()
250}
251
252fn fault(kind: ErrorKind, message: impl Into<String>, diagnostics: &Diagnostics) -> Error {
253    Error {
254        kind,
255        message: message.into(),
256        diagnostics: snapshot(diagnostics),
257    }
258}