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}