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