monoloop_connector_codex/
lib.rs1#![deny(missing_docs)]
19
20mod channel_binding;
21mod config;
22mod error;
23mod process;
24mod raw_dump;
25mod session;
26
27pub use channel_binding::{codex_channel_binding, CodexConnectorFactory};
28pub use config::{CodexAgentConfig, CodexSessionConfig};
29pub use error::CodexConnectorError;
30pub use raw_dump::CodexRawDump;
31pub use session::{CodexAgentHandle, CodexSession};
32
33use monoloop_connector::{
34 ConnectionCompletionHandle, ConnectionControlHandle, ConnectionEnd, ConnectionEndKind,
35 ConnectionOwnerWork, Connector, ConnectorDescriptor, ControlState, EndInitiator,
36 OpenConnection, OpenedRawConnection, PendingRawConnection, RawInputHandle, RawInputMessage,
37 RawOutputHandle,
38};
39use monoloop_contracts::{DialectBinding, DialectDescriptor, ExternalSessionId};
40use std::sync::Arc;
41use tokio::sync::{mpsc, oneshot};
42
43pub struct CodexConnector {
45 descriptor: ConnectorDescriptor,
46 default_config: CodexAgentConfig,
47}
48
49impl CodexConnector {
50 pub fn new() -> Self {
52 Self {
53 descriptor: ConnectorDescriptor::codex_acp(),
54 default_config: CodexAgentConfig::default(),
55 }
56 }
57
58 pub fn with_config(config: CodexAgentConfig) -> Self {
60 Self {
61 descriptor: ConnectorDescriptor::codex_acp(),
62 default_config: config,
63 }
64 }
65
66 pub async fn connect(
68 &self,
69 config: CodexAgentConfig,
70 ) -> Result<CodexAgentHandle, CodexConnectorError> {
71 CodexAgentHandle::connect(config).await
72 }
73}
74
75impl Default for CodexConnector {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl Connector for CodexConnector {
82 fn descriptor(&self) -> &ConnectorDescriptor {
83 &self.descriptor
84 }
85
86 fn begin_open(&self, request: OpenConnection) -> PendingRawConnection {
88 let mut config = self.default_config.clone();
89 if let Some(path) = parse_endpoint(&request.endpoint_ref) {
90 config.command = path;
91 config.args.clear();
92 }
93 let connection_id = request.connection_id.clone();
94 let control_state = ControlState::new();
95 let control = ConnectionControlHandle::new(Arc::clone(&control_state));
96 let control_open = control.clone();
97 PendingRawConnection::open_owned(connection_id, control, async move {
98 open_raw(config, request, control_open, control_state).await
99 })
100 }
101}
102
103fn parse_endpoint(endpoint_ref: &str) -> Option<std::path::PathBuf> {
104 let s = endpoint_ref
105 .strip_prefix("codex:")
106 .or_else(|| endpoint_ref.strip_prefix("openai-codex:"))
107 .unwrap_or(endpoint_ref);
108 if s == "stdio" || s.is_empty() {
109 None
110 } else {
111 Some(std::path::PathBuf::from(s))
112 }
113}
114
115async fn open_raw(
116 config: CodexAgentConfig,
117 request: OpenConnection,
118 control: ConnectionControlHandle,
119 control_state: Arc<ControlState>,
120) -> Result<(OpenedRawConnection, ConnectionOwnerWork), monoloop_contracts::ConnectorError> {
121 let mut agent = CodexAgentHandle::connect(config.clone())
122 .await
123 .map_err(|e| e.into_connector_error())?;
124
125 let session = if let Some(ref ext) = request.external_session_id {
126 agent
127 .session_load(ext.as_str(), &config.cwd)
128 .await
129 .map_err(|e| e.into_connector_error())?
130 } else {
131 let mut session_cfg = CodexSessionConfig::new(&config.cwd);
133 if let Some(mcp) = request
134 .session_attachment
135 .as_ref()
136 .and_then(|a| a.initial_mcp.as_ref())
137 {
138 session_cfg.mcp_servers = serde_json::json!([{
139 "name": mcp.server_name,
140 "type": "http",
141 "url": mcp.expose_capability_url(),
142 }]);
143 }
144 agent
145 .session_new(session_cfg)
146 .await
147 .map_err(|e| e.into_connector_error())?
148 };
149
150 let dialect = DialectBinding::negotiated(DialectDescriptor::codex_acp("1"));
151 let max_chunk = request.limits.buffers.max_chunk_bytes.max(1);
152 let in_capacity = (request.limits.buffers.max_queued_input_bytes.max(1) / max_chunk).max(1);
153 let out_capacity = (request.limits.buffers.max_queued_output_bytes.max(1) / max_chunk)
154 .max(1)
155 .min(config.max_output_queue.max(1));
156 let (in_tx, mut in_rx) = mpsc::channel::<RawInputMessage>(in_capacity);
157 let (out_tx, out_rx) = mpsc::channel(out_capacity);
158 let (end_tx, end_rx) = oneshot::channel::<ConnectionEnd>();
159
160 let input = RawInputHandle::new(
161 request.connection_id.clone(),
162 in_tx,
163 Arc::clone(&control_state),
164 max_chunk,
165 );
166 let output = Arc::new(RawOutputHandle::new(
167 request.connection_id.clone(),
168 out_rx,
169 Arc::clone(&control_state),
170 ));
171 let completion = ConnectionCompletionHandle::new(end_rx);
172
173 let connection_id = request.connection_id.clone();
174 let external_session_id = Some(ExternalSessionId::new(session.session_id.clone()));
175
176 let mut updates = agent.take_updates();
180 let control_wait = control.clone();
181 let owner_work = ConnectionOwnerWork::new(async move {
182 let mut joins = tokio::task::JoinSet::new();
183 let out_pump = out_tx;
184 joins.spawn(async move {
185 while let Some(bytes) = updates.recv().await {
186 if out_pump.send(bytes).await.is_err() {
187 break;
188 }
189 }
190 });
191
192 let mut bytes_accepted = 0u64;
193 let (end_kind, initiated, safe_err) = loop {
194 tokio::select! {
195 biased;
196 _ = control_wait.interrupted() => {
197 let kind = if control_state.terminate_requested() {
198 ConnectionEndKind::Terminated
199 } else {
200 ConnectionEndKind::Cancelled
201 };
202 let _ = session.cancel().await;
203 break (kind, EndInitiator::LocalControl, None);
204 }
205 msg = in_rx.recv() => {
206 match msg {
207 Some(RawInputMessage::Bytes(b)) => {
208 bytes_accepted += b.len() as u64;
209 let text = String::from_utf8_lossy(&b).into_owned();
210 if text.trim().is_empty() {
211 continue;
212 }
213 if let Err(e) = session.prompt_text(text).await {
214 break (
215 ConnectionEndKind::TransportFailure,
216 EndInitiator::LocalTransport,
217 Some(safe_prompt_error(&e)),
218 );
219 }
220 }
221 Some(RawInputMessage::Finish) | None => {
222 break (
225 ConnectionEndKind::LocalShutdown,
226 EndInitiator::LocalControl,
227 None,
228 );
229 }
230 }
231 }
232 }
233 };
234
235 agent.shutdown().await;
236 while joins.join_next().await.is_some() {}
237 control_state.mark_terminal();
238 let _ = end_tx.send(ConnectionEnd {
239 connection_id: connection_id.clone(),
240 kind: end_kind,
241 initiated_by: initiated,
242 bytes_accepted,
243 bytes_received: 0,
244 safe_transport_error: safe_err,
245 });
246 });
247
248 Ok((
249 OpenedRawConnection {
250 connection_id: request.connection_id,
251 external_session_id,
252 dialect,
253 input,
254 output,
255 control,
256 completion,
257 },
258 owner_work,
259 ))
260}
261
262fn safe_prompt_error(e: &CodexConnectorError) -> String {
263 match e.kind {
264 monoloop_contracts::ConnectorErrorKind::DeadlineExceeded => {
265 "prompt_rpc_deadline_exceeded".into()
266 }
267 monoloop_contracts::ConnectorErrorKind::Cancelled => "prompt_cancelled".into(),
268 monoloop_contracts::ConnectorErrorKind::ConnectionFailed => {
269 "prompt_connection_failed".into()
270 }
271 monoloop_contracts::ConnectorErrorKind::ProtocolFailed => "prompt_protocol_failed".into(),
272 monoloop_contracts::ConnectorErrorKind::SessionFailed => "prompt_session_failed".into(),
273 _ => "prompt_failed".into(),
274 }
275}