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}