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