codex_runtime/runtime/client/
mod.rs1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3
4use thiserror::Error;
5
6use crate::runtime::api::{tool_use_hooks, PromptRunError, PromptRunParams, PromptRunResult};
7use crate::runtime::core::{Runtime, RuntimeConfig};
8#[cfg(test)]
9use crate::runtime::errors::RpcError;
10use crate::runtime::errors::RuntimeError;
11use crate::runtime::transport::StdioProcessSpec;
12
13mod compat_guard;
14mod config;
15mod profile;
16mod session;
17
18pub use compat_guard::{CompatibilityGuard, SemVerTriplet};
19pub use config::ClientConfig;
20pub use profile::{RunProfile, SessionConfig};
21pub use session::Session;
22
23use compat_guard::validate_runtime_compatibility;
24use profile::{profile_to_prompt_params_with_hooks, session_thread_start_params};
25
26#[derive(Clone)]
27pub struct Client {
28 runtime: Runtime,
29 config: ClientConfig,
30 tool_use_loop_started: Arc<AtomicBool>,
31}
32
33impl Client {
34 pub async fn connect_default() -> Result<Self, ClientError> {
38 Self::connect(ClientConfig::new()).await
39 }
40
41 pub async fn connect(config: ClientConfig) -> Result<Self, ClientError> {
45 let mut process = StdioProcessSpec::new(config.cli_bin.clone());
46 process.args = vec!["app-server".to_owned()];
47
48 let runtime = Runtime::spawn_local(
49 RuntimeConfig::new(process)
50 .with_hooks(config.hooks.clone())
51 .with_initialize_capabilities(config.initialize_capabilities),
52 )
53 .await?;
54 if let Err(compatibility) =
55 validate_runtime_compatibility(&runtime, &config.compatibility_guard)
56 {
57 if let Err(shutdown) = runtime.shutdown().await {
58 return Err(ClientError::CompatibilityValidationWithShutdown {
59 compatibility: Box::new(compatibility),
60 shutdown,
61 });
62 }
63 return Err(compatibility);
64 }
65
66 let tool_use_loop_started = Arc::new(AtomicBool::new(false));
70 let client = Self {
71 runtime,
72 config,
73 tool_use_loop_started,
74 };
75 client.ensure_tool_use_hook_loop(client.config.hooks.has_pre_tool_use_hooks());
76 Ok(client)
77 }
78
79 pub async fn run(
82 &self,
83 cwd: impl Into<String>,
84 prompt: impl Into<String>,
85 ) -> Result<PromptRunResult, PromptRunError> {
86 self.runtime.run_prompt_simple(cwd, prompt).await
87 }
88
89 pub async fn run_with(
92 &self,
93 params: PromptRunParams,
94 ) -> Result<PromptRunResult, PromptRunError> {
95 self.runtime.run_prompt(params).await
96 }
97
98 pub async fn run_with_profile(
102 &self,
103 cwd: impl Into<String>,
104 prompt: impl Into<String>,
105 profile: RunProfile,
106 ) -> Result<PromptRunResult, PromptRunError> {
107 let (params, hooks) = profile_to_prompt_params_with_hooks(cwd.into(), prompt, profile);
108 self.ensure_tool_use_hook_loop(hooks.has_pre_tool_use_hooks());
109 self.runtime
110 .run_prompt_with_hooks(params, Some(&hooks))
111 .await
112 }
113
114 pub async fn start_session(&self, config: SessionConfig) -> Result<Session, PromptRunError> {
118 self.ensure_tool_use_hook_loop(config.hooks.has_pre_tool_use_hooks());
119 let thread = self
120 .runtime
121 .thread_start_with_hooks(session_thread_start_params(&config), Some(&config.hooks))
122 .await?;
123
124 Ok(Session::new(
125 self.runtime.clone(),
126 thread.thread_id,
127 config,
128 Arc::clone(&self.tool_use_loop_started),
129 ))
130 }
131
132 pub async fn resume_session(
136 &self,
137 thread_id: &str,
138 config: SessionConfig,
139 ) -> Result<Session, PromptRunError> {
140 self.ensure_tool_use_hook_loop(config.hooks.has_pre_tool_use_hooks());
141 let thread = self
142 .runtime
143 .thread_resume_with_hooks(
144 thread_id,
145 session_thread_start_params(&config),
146 Some(&config.hooks),
147 )
148 .await?;
149
150 Ok(Session::new(
151 self.runtime.clone(),
152 thread.thread_id,
153 config,
154 Arc::clone(&self.tool_use_loop_started),
155 ))
156 }
157
158 pub fn runtime(&self) -> &Runtime {
161 &self.runtime
162 }
163
164 pub fn config(&self) -> &ClientConfig {
167 &self.config
168 }
169
170 pub async fn shutdown(&self) -> Result<(), RuntimeError> {
173 self.runtime.shutdown().await
174 }
175
176 fn ensure_tool_use_hook_loop(&self, needs_loop: bool) {
177 if !needs_loop {
178 return;
179 }
180 if self
181 .tool_use_loop_started
182 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
183 .is_ok()
184 {
185 tokio::spawn(tool_use_hooks::run_tool_use_approval_loop(
186 self.runtime.clone(),
187 ));
188 }
189 }
190}
191
192#[derive(Clone, Debug, Error, PartialEq, Eq)]
193pub enum ClientError {
194 #[error("failed to read current directory: {0}")]
195 CurrentDir(String),
196
197 #[error("initialize response missing userAgent")]
198 MissingInitializeUserAgent,
199
200 #[error("initialize response has unsupported userAgent format: {0}")]
201 InvalidInitializeUserAgent(String),
202
203 #[error("incompatible codex runtime version: detected={detected} required>={required} userAgent={user_agent}")]
204 IncompatibleCodexVersion {
205 detected: String,
206 required: String,
207 user_agent: String,
208 },
209
210 #[error(
211 "compatibility validation failed: {compatibility}; runtime shutdown failed: {shutdown}"
212 )]
213 CompatibilityValidationWithShutdown {
214 compatibility: Box<ClientError>,
215 shutdown: RuntimeError,
216 },
217
218 #[error("runtime error: {0}")]
219 Runtime(#[from] RuntimeError),
220}
221
222#[cfg(test)]
223fn parse_initialize_user_agent(value: &str) -> Option<(String, SemVerTriplet)> {
224 compat_guard::parse_initialize_user_agent(value)
225}
226
227#[cfg(test)]
228fn session_prompt_params(config: &SessionConfig, prompt: impl Into<String>) -> PromptRunParams {
229 profile::session_prompt_params(config, prompt)
230}
231
232#[cfg(test)]
233fn profile_to_prompt_params(
234 cwd: String,
235 prompt: impl Into<String>,
236 profile: RunProfile,
237) -> PromptRunParams {
238 profile::profile_to_prompt_params(cwd, prompt, profile)
239}
240
241#[cfg(test)]
242fn ensure_session_open_for_prompt(closed: bool) -> Result<(), PromptRunError> {
243 session::ensure_session_open_for_prompt(closed)
244}
245
246#[cfg(test)]
247fn ensure_session_open_for_rpc(closed: bool) -> Result<(), RpcError> {
248 session::ensure_session_open_for_rpc(closed)
249}
250
251#[cfg(test)]
252mod tests;