1use std::sync::Arc;
2use std::time::Duration;
3
4use codex_code_mode_protocol::CellId;
5use codex_code_mode_protocol::CodeModeNestedToolCall;
6use codex_code_mode_protocol::CodeModeSession;
7use codex_code_mode_protocol::CodeModeSessionDelegate;
8use codex_code_mode_protocol::CodeModeSessionProvider;
9use codex_code_mode_protocol::CodeModeSessionProviderFuture;
10use codex_code_mode_protocol::CodeModeSessionResultFuture;
11use codex_code_mode_protocol::CodeModeToolKind;
12use codex_code_mode_protocol::DEFAULT_EXEC_YIELD_TIME_MS;
13use codex_code_mode_protocol::ExecuteRequest;
14use codex_code_mode_protocol::ExecuteToPendingOutcome;
15use codex_code_mode_protocol::FunctionCallOutputContentItem;
16use codex_code_mode_protocol::ImageDetail;
17use codex_code_mode_protocol::NotificationFuture;
18use codex_code_mode_protocol::RuntimeResponse;
19use codex_code_mode_protocol::StartedCell;
20use codex_code_mode_protocol::ToolInvocationFuture;
21use codex_code_mode_protocol::WaitOutcome;
22use codex_code_mode_protocol::WaitRequest;
23use codex_code_mode_protocol::WaitToPendingOutcome;
24use codex_code_mode_protocol::WaitToPendingRequest;
25use serde_json::Value as JsonValue;
26use tokio::sync::oneshot;
27use tokio_util::sync::CancellationToken;
28
29use crate::session_runtime as runtime;
30use crate::session_runtime::SessionRuntime;
31
32const YIELD_GRACE_PERIOD: Duration = Duration::from_secs(1);
33const MIN_YIELD_TIME_FOR_GRACE: Duration = Duration::from_secs(10);
34
35fn yield_timeout(yield_time_ms: u64) -> Duration {
36 let yield_time = Duration::from_millis(yield_time_ms);
37 if yield_time >= MIN_YIELD_TIME_FOR_GRACE {
38 yield_time.saturating_add(YIELD_GRACE_PERIOD)
39 } else {
40 yield_time
41 }
42}
43
44pub struct NoopCodeModeSessionDelegate;
45
46impl CodeModeSessionDelegate for NoopCodeModeSessionDelegate {
47 fn invoke_tool<'a>(
48 &'a self,
49 _invocation: CodeModeNestedToolCall,
50 cancellation_token: CancellationToken,
51 ) -> ToolInvocationFuture<'a> {
52 Box::pin(async move {
53 cancellation_token.cancelled().await;
54 Err("code mode nested tools are unavailable".to_string())
55 })
56 }
57
58 fn notify<'a>(
59 &'a self,
60 _call_id: String,
61 _cell_id: CellId,
62 _text: String,
63 _cancellation_token: CancellationToken,
64 ) -> NotificationFuture<'a> {
65 Box::pin(async { Ok(()) })
66 }
67
68 fn cell_closed(&self, _cell_id: &CellId) {}
69}
70
71#[derive(Default)]
72pub struct InProcessCodeModeSessionProvider;
73
74impl CodeModeSessionProvider for InProcessCodeModeSessionProvider {
75 fn create_session<'a>(
76 &'a self,
77 delegate: Arc<dyn CodeModeSessionDelegate>,
78 ) -> CodeModeSessionProviderFuture<'a> {
79 Box::pin(async move {
80 let session: Arc<dyn CodeModeSession> =
81 Arc::new(InProcessCodeModeSession::with_delegate(delegate));
82 Ok(session)
83 })
84 }
85}
86
87pub struct InProcessCodeModeSession {
88 runtime: SessionRuntime<ProtocolDelegate>,
89}
90
91impl InProcessCodeModeSession {
92 pub fn new() -> Self {
93 Self::with_delegate(Arc::new(NoopCodeModeSessionDelegate))
94 }
95
96 pub fn with_delegate(delegate: Arc<dyn CodeModeSessionDelegate>) -> Self {
97 Self {
98 runtime: SessionRuntime::new(Arc::new(ProtocolDelegate { delegate })),
99 }
100 }
101
102 pub fn with_delegate_and_task_failure_handler(
103 delegate: Arc<dyn CodeModeSessionDelegate>,
104 task_failure_handler: Arc<dyn Fn(String) + Send + Sync>,
105 ) -> Self {
106 Self {
107 runtime: SessionRuntime::new_with_task_failure_handler(
108 Arc::new(ProtocolDelegate { delegate }),
109 Some(task_failure_handler),
110 ),
111 }
112 }
113
114 pub async fn execute(&self, request: ExecuteRequest) -> Result<StartedCell, String> {
115 let yield_time_ms = request.yield_time_ms.unwrap_or(DEFAULT_EXEC_YIELD_TIME_MS);
116 let started = self
117 .runtime
118 .execute(
119 runtime_request(request),
120 runtime::ObserveMode::YieldAfter(yield_timeout(yield_time_ms)),
121 )
122 .await
123 .map_err(|error| error.to_string())?;
124 let cell_id = protocol_cell_id(&started.cell_id);
125 let response_cell_id = cell_id.clone();
126 let (response_tx, response_rx) = oneshot::channel();
127 tokio::spawn(async move {
128 let response = started
129 .initial_event()
130 .await
131 .map_err(|error| error.to_string())
132 .and_then(|event| runtime_response(&response_cell_id, event));
133 let _ = response_tx.send(response);
134 });
135 Ok(StartedCell::from_result_receiver(cell_id, response_rx))
136 }
137
138 pub async fn execute_to_pending(
139 &self,
140 request: ExecuteRequest,
141 ) -> Result<ExecuteToPendingOutcome, String> {
142 let started = self
143 .runtime
144 .execute(
145 runtime_request(request),
146 runtime::ObserveMode::PendingFrontier,
147 )
148 .await
149 .map_err(|error| error.to_string())?;
150 let cell_id = protocol_cell_id(&started.cell_id);
151 let event = started
152 .initial_event()
153 .await
154 .map_err(|error| error.to_string())?;
155 pending_outcome(&cell_id, event)
156 }
157
158 pub async fn wait(&self, request: WaitRequest) -> Result<WaitOutcome, String> {
159 self.begin_wait(request).await.await
160 }
161
162 async fn begin_wait(
163 &self,
164 request: WaitRequest,
165 ) -> CodeModeSessionResultFuture<'static, WaitOutcome> {
166 let WaitRequest {
167 cell_id,
168 yield_time_ms,
169 } = request;
170 let runtime_cell_id = runtime_cell_id(&cell_id);
171 match self
172 .runtime
173 .begin_observe(
174 &runtime_cell_id,
175 runtime::ObserveMode::YieldAfter(yield_timeout(yield_time_ms)),
176 )
177 .await
178 {
179 Ok(pending_event) => Box::pin(async move {
180 match pending_event.event().await {
181 Ok(event) => Ok(WaitOutcome::LiveCell(runtime_response(&cell_id, event)?)),
182 Err(runtime::Error::MissingCell(_) | runtime::Error::ClosedCell(_)) => {
183 Ok(WaitOutcome::MissingCell(missing_cell_response(cell_id)))
184 }
185 Err(error) => Err(error.to_string()),
186 }
187 }),
188 Err(runtime::Error::MissingCell(_) | runtime::Error::ClosedCell(_)) => {
189 missing_wait(cell_id)
190 }
191 Err(error) => Box::pin(async move { Err(error.to_string()) }),
192 }
193 }
194
195 pub async fn terminate(&self, cell_id: CellId) -> Result<WaitOutcome, String> {
196 match self.runtime.terminate(&runtime_cell_id(&cell_id)).await {
197 Ok(event) => Ok(WaitOutcome::LiveCell(runtime_response(&cell_id, event)?)),
198 Err(runtime::Error::MissingCell(_) | runtime::Error::ClosedCell(_)) => {
199 Ok(WaitOutcome::MissingCell(missing_cell_response(cell_id)))
200 }
201 Err(error) => Err(error.to_string()),
202 }
203 }
204
205 pub async fn wait_to_pending(
206 &self,
207 request: WaitToPendingRequest,
208 ) -> Result<WaitToPendingOutcome, String> {
209 let cell_id = request.cell_id;
210 match self
211 .runtime
212 .observe(
213 &runtime_cell_id(&cell_id),
214 runtime::ObserveMode::PendingFrontier,
215 )
216 .await
217 {
218 Ok(event) => Ok(WaitToPendingOutcome::LiveCell(pending_outcome(
219 &cell_id, event,
220 )?)),
221 Err(runtime::Error::MissingCell(_) | runtime::Error::ClosedCell(_)) => Ok(
222 WaitToPendingOutcome::MissingCell(missing_cell_response(cell_id)),
223 ),
224 Err(error) => Err(error.to_string()),
225 }
226 }
227
228 pub async fn shutdown(&self) -> Result<(), String> {
229 self.runtime
230 .shutdown()
231 .await
232 .map_err(|error| error.to_string())
233 }
234}
235
236impl Default for InProcessCodeModeSession {
237 fn default() -> Self {
238 Self::new()
239 }
240}
241
242impl CodeModeSession for InProcessCodeModeSession {
243 fn execute<'a>(
244 &'a self,
245 request: ExecuteRequest,
246 ) -> CodeModeSessionResultFuture<'a, StartedCell> {
247 Box::pin(InProcessCodeModeSession::execute(self, request))
248 }
249
250 fn wait<'a>(&'a self, request: WaitRequest) -> CodeModeSessionResultFuture<'a, WaitOutcome> {
251 Box::pin(InProcessCodeModeSession::wait(self, request))
252 }
253
254 fn terminate<'a>(&'a self, cell_id: CellId) -> CodeModeSessionResultFuture<'a, WaitOutcome> {
255 Box::pin(InProcessCodeModeSession::terminate(self, cell_id))
256 }
257
258 fn shutdown<'a>(&'a self) -> CodeModeSessionResultFuture<'a, ()> {
259 Box::pin(InProcessCodeModeSession::shutdown(self))
260 }
261}
262
263struct ProtocolDelegate {
264 delegate: Arc<dyn CodeModeSessionDelegate>,
265}
266
267impl runtime::SessionRuntimeDelegate for ProtocolDelegate {
268 async fn invoke_tool(
269 &self,
270 invocation: runtime::NestedToolCall,
271 cancellation_token: CancellationToken,
272 ) -> Result<JsonValue, String> {
273 self.delegate
274 .invoke_tool(
275 CodeModeNestedToolCall {
276 cell_id: protocol_cell_id(&invocation.cell_id),
277 runtime_tool_call_id: invocation.runtime_tool_call_id,
278 tool_name: codex_protocol::ToolName {
279 name: invocation.tool_name.name,
280 namespace: invocation.tool_name.namespace,
281 },
282 tool_kind: match invocation.tool_kind {
283 runtime::ToolKind::Function => CodeModeToolKind::Function,
284 runtime::ToolKind::Freeform => CodeModeToolKind::Freeform,
285 },
286 input: invocation.input,
287 },
288 cancellation_token,
289 )
290 .await
291 }
292
293 async fn notify(
294 &self,
295 call_id: String,
296 cell_id: runtime::CellId,
297 text: String,
298 cancellation_token: CancellationToken,
299 ) -> Result<(), String> {
300 self.delegate
301 .notify(
302 call_id,
303 protocol_cell_id(&cell_id),
304 text,
305 cancellation_token,
306 )
307 .await
308 }
309
310 fn cell_closed(&self, cell_id: &runtime::CellId) {
311 self.delegate.cell_closed(&protocol_cell_id(cell_id));
312 }
313}
314
315fn runtime_request(request: ExecuteRequest) -> runtime::CreateCellRequest {
316 runtime::CreateCellRequest {
317 tool_call_id: request.tool_call_id,
318 enabled_tools: request
319 .enabled_tools
320 .into_iter()
321 .map(|definition| runtime::ToolDefinition {
322 name: definition.name,
323 tool_name: runtime::ToolName {
324 name: definition.tool_name.name,
325 namespace: definition.tool_name.namespace,
326 },
327 description: definition.description,
328 kind: match definition.kind {
329 CodeModeToolKind::Function => runtime::ToolKind::Function,
330 CodeModeToolKind::Freeform => runtime::ToolKind::Freeform,
331 },
332 })
333 .collect(),
334 source: request.source,
335 }
336}
337
338fn runtime_cell_id(cell_id: &CellId) -> runtime::CellId {
339 runtime::CellId::new(cell_id.as_str())
340}
341
342fn protocol_cell_id(cell_id: &runtime::CellId) -> CellId {
343 CellId::new(cell_id.as_str().to_string())
344}
345
346fn pending_outcome(
347 cell_id: &CellId,
348 event: runtime::CellEvent,
349) -> Result<ExecuteToPendingOutcome, String> {
350 match event {
351 runtime::CellEvent::Pending {
352 content_items,
353 pending_tool_call_ids,
354 } => Ok(ExecuteToPendingOutcome::Pending {
355 cell_id: cell_id.clone(),
356 content_items: content_items.into_iter().map(output_item).collect(),
357 pending_tool_call_ids,
358 }),
359 event => Ok(ExecuteToPendingOutcome::Completed(runtime_response(
360 cell_id, event,
361 )?)),
362 }
363}
364
365fn runtime_response(
366 cell_id: &CellId,
367 event: runtime::CellEvent,
368) -> Result<RuntimeResponse, String> {
369 match event {
370 runtime::CellEvent::Yielded { content_items } => Ok(RuntimeResponse::Yielded {
371 cell_id: cell_id.clone(),
372 content_items: content_items.into_iter().map(output_item).collect(),
373 }),
374 runtime::CellEvent::Completed {
375 content_items,
376 error_text,
377 } => Ok(RuntimeResponse::Result {
378 cell_id: cell_id.clone(),
379 content_items: content_items.into_iter().map(output_item).collect(),
380 error_text,
381 }),
382 runtime::CellEvent::Terminated { content_items } => Ok(RuntimeResponse::Terminated {
383 cell_id: cell_id.clone(),
384 content_items: content_items.into_iter().map(output_item).collect(),
385 }),
386 runtime::CellEvent::Pending { .. } => {
387 Err("cell returned a pending frontier unexpectedly".to_string())
388 }
389 }
390}
391
392fn output_item(item: runtime::OutputItem) -> FunctionCallOutputContentItem {
393 match item {
394 runtime::OutputItem::Text { text } => FunctionCallOutputContentItem::InputText { text },
395 runtime::OutputItem::Image { image_url, detail } => {
396 FunctionCallOutputContentItem::InputImage {
397 image_url,
398 detail: detail.map(|detail| match detail {
399 runtime::ImageDetail::Auto => ImageDetail::Auto,
400 runtime::ImageDetail::Low => ImageDetail::Low,
401 runtime::ImageDetail::High => ImageDetail::High,
402 runtime::ImageDetail::Original => ImageDetail::Original,
403 }),
404 }
405 }
406 runtime::OutputItem::Audio { audio_url } => {
407 FunctionCallOutputContentItem::InputAudio { audio_url }
408 }
409 }
410}
411
412fn missing_cell_response(cell_id: CellId) -> RuntimeResponse {
413 RuntimeResponse::Result {
414 error_text: Some(format!("exec cell {cell_id} not found")),
415 cell_id,
416 content_items: Vec::new(),
417 }
418}
419
420fn missing_wait(cell_id: CellId) -> CodeModeSessionResultFuture<'static, WaitOutcome> {
421 Box::pin(async move { Ok(WaitOutcome::MissingCell(missing_cell_response(cell_id))) })
422}
423
424#[cfg(test)]
425#[path = "service_tests.rs"]
426mod tests;
427
428#[cfg(test)]
429#[path = "service_contract_tests.rs"]
430mod contract_tests;