Skip to main content

codex_code_mode/
service.rs

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;