codex_runtime/runtime/client/
mod.rs1use thiserror::Error;
2
3use crate::runtime::api::{PromptRunError, PromptRunParams, PromptRunResult};
4use crate::runtime::core::{Runtime, RuntimeConfig};
5use crate::runtime::errors::RuntimeError;
6use crate::runtime::transport::StdioProcessSpec;
7
8mod compat_guard;
9mod config;
10mod profile;
11mod session;
12
13pub use compat_guard::{CompatibilityGuard, SemVerTriplet};
14pub use config::ClientConfig;
15pub use profile::{RunProfile, SessionConfig};
16pub use session::Session;
17
18use compat_guard::validate_runtime_compatibility;
19use profile::{prepared_prompt_run_from_profile, session_thread_start_params};
20
21#[derive(Clone)]
22pub struct Client {
23 runtime: Runtime,
24 config: ClientConfig,
25}
26
27impl Client {
28 pub async fn connect_default() -> Result<Self, ClientError> {
32 Self::connect(ClientConfig::new()).await
33 }
34
35 pub async fn connect(config: ClientConfig) -> Result<Self, ClientError> {
39 let mut process = StdioProcessSpec::new(config.cli_bin.clone());
40 process.args = vec!["app-server".to_owned()];
41 process.args.extend(config.app_server_args.iter().cloned());
42 process.env = config.process_env.clone();
43 process.cwd = config.process_cwd.clone();
44
45 let runtime = Runtime::spawn_local(
46 RuntimeConfig::new(process)
47 .with_hooks(config.hooks.clone())
48 .with_initialize_capabilities(config.initialize_capabilities),
49 )
50 .await?;
51 if let Err(compatibility) =
52 validate_runtime_compatibility(&runtime, &config.compatibility_guard)
53 {
54 if let Err(shutdown) = runtime.shutdown().await {
55 return Err(ClientError::CompatibilityValidationWithShutdown {
56 compatibility: Box::new(compatibility),
57 shutdown,
58 });
59 }
60 return Err(compatibility);
61 }
62
63 Ok(Self { runtime, config })
64 }
65
66 pub async fn run(
69 &self,
70 cwd: impl Into<String>,
71 prompt: impl Into<String>,
72 ) -> Result<PromptRunResult, PromptRunError> {
73 self.runtime.run_prompt_simple(cwd, prompt).await
74 }
75
76 pub async fn run_with(
79 &self,
80 params: PromptRunParams,
81 ) -> Result<PromptRunResult, PromptRunError> {
82 self.runtime.run_prompt(params).await
83 }
84
85 pub async fn run_with_profile(
89 &self,
90 cwd: impl Into<String>,
91 prompt: impl Into<String>,
92 profile: RunProfile,
93 ) -> Result<PromptRunResult, PromptRunError> {
94 let prepared = prepared_prompt_run_from_profile(cwd.into(), prompt, profile);
95 self.runtime
96 .run_prompt_with_hooks(prepared.params, Some(prepared.hooks.as_ref()))
97 .await
98 }
99
100 pub async fn start_session(&self, config: SessionConfig) -> Result<Session, PromptRunError> {
104 let thread = self
105 .runtime
106 .thread_start_with_hooks(session_thread_start_params(&config), Some(&config.hooks))
107 .await?;
108
109 Ok(Session::new(self.runtime.clone(), thread.thread_id, config))
110 }
111
112 pub async fn resume_session(
116 &self,
117 thread_id: &str,
118 config: SessionConfig,
119 ) -> Result<Session, PromptRunError> {
120 let thread = self
121 .runtime
122 .thread_resume_with_hooks(
123 thread_id,
124 session_thread_start_params(&config),
125 Some(&config.hooks),
126 )
127 .await?;
128
129 Ok(Session::new(self.runtime.clone(), thread.thread_id, config))
130 }
131
132 pub fn runtime(&self) -> &Runtime {
135 &self.runtime
136 }
137
138 pub fn config(&self) -> &ClientConfig {
141 &self.config
142 }
143
144 pub async fn shutdown(&self) -> Result<(), RuntimeError> {
147 self.runtime.shutdown().await
148 }
149}
150
151#[derive(Clone, Debug, Error, PartialEq, Eq)]
152pub enum ClientError {
153 #[error("failed to read current directory: {0}")]
154 CurrentDir(String),
155
156 #[error("initialize response missing userAgent")]
157 MissingInitializeUserAgent,
158
159 #[error("initialize response has unsupported userAgent format: {0}")]
160 InvalidInitializeUserAgent(String),
161
162 #[error("incompatible codex runtime version: detected={detected} required>={required} userAgent={user_agent}")]
163 IncompatibleCodexVersion {
164 detected: String,
165 required: String,
166 user_agent: String,
167 },
168
169 #[error(
170 "compatibility validation failed: {compatibility}; runtime shutdown failed: {shutdown}"
171 )]
172 CompatibilityValidationWithShutdown {
173 compatibility: Box<ClientError>,
174 shutdown: RuntimeError,
175 },
176
177 #[error("runtime error: {0}")]
178 Runtime(#[from] RuntimeError),
179}
180
181#[cfg(test)]
182fn parse_initialize_user_agent(value: &str) -> Option<(String, SemVerTriplet)> {
183 compat_guard::parse_initialize_user_agent(value)
184}
185
186#[cfg(test)]
187fn session_prompt_params(config: &SessionConfig, prompt: impl Into<String>) -> PromptRunParams {
188 profile::session_prompt_params(config, prompt)
189}
190
191#[cfg(test)]
192fn profile_to_prompt_params(
193 cwd: String,
194 prompt: impl Into<String>,
195 profile: RunProfile,
196) -> PromptRunParams {
197 profile::profile_to_prompt_params(cwd, prompt, profile)
198}
199
200#[cfg(test)]
201mod tests;