Skip to main content

codex_runtime/runtime/client/
session.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3
4use tokio::sync::Mutex;
5
6use crate::runtime::api::{PromptRunError, PromptRunParams, PromptRunResult};
7use crate::runtime::core::Runtime;
8use crate::runtime::errors::RpcError;
9use crate::runtime::hooks::merge_hook_configs;
10
11use super::profile::{profile_to_prompt_params_with_hooks, session_prompt_params};
12use super::{RunProfile, SessionConfig};
13
14#[derive(Clone)]
15pub struct Session {
16    runtime: Runtime,
17    pub thread_id: String,
18    pub config: SessionConfig,
19    closed: Arc<AtomicBool>,
20    close_result: Arc<Mutex<Option<Result<(), RpcError>>>>,
21}
22
23impl Session {
24    pub(super) fn new(runtime: Runtime, thread_id: String, config: SessionConfig) -> Self {
25        Self {
26            runtime,
27            thread_id,
28            config,
29            closed: Arc::new(AtomicBool::new(false)),
30            close_result: Arc::new(Mutex::new(None)),
31        }
32    }
33
34    /// Returns true when this local session handle is closed.
35    /// Allocation: none. Complexity: O(1).
36    pub fn is_closed(&self) -> bool {
37        self.closed.load(Ordering::Acquire)
38    }
39
40    /// Continue this session with one prompt.
41    /// Side effects: sends turn/start RPC calls on one already-loaded thread.
42    /// Allocation: PromptRunParams clone payloads (cwd/model/sandbox/attachments). Complexity: O(n), n = attachment count + prompt length.
43    pub async fn ask(&self, prompt: impl Into<String>) -> Result<PromptRunResult, PromptRunError> {
44        ensure_session_open_for_prompt(self.is_closed())?;
45        self.runtime
46            .run_prompt_on_loaded_thread_with_hooks(
47                &self.thread_id,
48                session_prompt_params(&self.config, prompt),
49                Some(&self.config.hooks),
50            )
51            .await
52    }
53
54    /// Continue this session with one prompt while overriding selected turn options.
55    /// Side effects: sends turn/start RPC calls on one already-loaded thread.
56    /// Allocation: depends on caller-provided params. Complexity: O(1) wrapper.
57    pub async fn ask_with(
58        &self,
59        params: PromptRunParams,
60    ) -> Result<PromptRunResult, PromptRunError> {
61        ensure_session_open_for_prompt(self.is_closed())?;
62        self.runtime
63            .run_prompt_on_loaded_thread_with_hooks(
64                &self.thread_id,
65                params,
66                Some(&self.config.hooks),
67            )
68            .await
69    }
70
71    /// Continue this session with one prompt using one explicit profile override.
72    /// Side effects: sends turn/start RPC calls on one already-loaded thread.
73    /// Allocation: moves profile-owned Strings/vectors + one prompt String. Complexity: O(n), n = attachment count + field sizes.
74    pub async fn ask_with_profile(
75        &self,
76        prompt: impl Into<String>,
77        profile: RunProfile,
78    ) -> Result<PromptRunResult, PromptRunError> {
79        ensure_session_open_for_prompt(self.is_closed())?;
80        let (params, profile_hooks) =
81            profile_to_prompt_params_with_hooks(self.config.cwd.clone(), prompt, profile);
82        let merged_hooks = merge_hook_configs(&self.config.hooks, &profile_hooks);
83        self.runtime
84            .run_prompt_on_loaded_thread_with_hooks(&self.thread_id, params, Some(&merged_hooks))
85            .await
86    }
87
88    /// Return current session default profile snapshot.
89    /// Allocation: clones Strings/attachments. Complexity: O(n), n = attachment count + string sizes.
90    pub fn profile(&self) -> RunProfile {
91        self.config.profile()
92    }
93
94    /// Interrupt one in-flight turn in this session.
95    /// Side effects: sends turn/interrupt RPC call to app-server.
96    /// Allocation: one small JSON payload in runtime layer. Complexity: O(1).
97    pub async fn interrupt_turn(&self, turn_id: &str) -> Result<(), RpcError> {
98        ensure_session_open_for_rpc(self.is_closed())?;
99        self.runtime.turn_interrupt(&self.thread_id, turn_id).await
100    }
101
102    /// Archive this session on server side.
103    /// Side effects: sends thread/archive RPC call to app-server.
104    /// Allocation: one small JSON payload in runtime layer. Complexity: O(1).
105    pub async fn close(&self) -> Result<(), RpcError> {
106        let mut guard = self.close_result.lock().await;
107        if let Some(result) = guard.as_ref() {
108            return result.clone();
109        }
110
111        self.closed.store(true, Ordering::Release);
112        let result = self.runtime.thread_archive(&self.thread_id).await;
113        *guard = Some(result.clone());
114        result
115    }
116}
117
118pub(super) fn ensure_session_open_for_prompt(closed: bool) -> Result<(), PromptRunError> {
119    if closed {
120        return Err(PromptRunError::Rpc(RpcError::InvalidRequest(
121            "session is closed".to_owned(),
122        )));
123    }
124    Ok(())
125}
126
127pub(super) fn ensure_session_open_for_rpc(closed: bool) -> Result<(), RpcError> {
128    if closed {
129        return Err(RpcError::InvalidRequest("session is closed".to_owned()));
130    }
131    Ok(())
132}