1use std::sync::Arc;
4
5use tokio::sync::mpsc;
6
7use crate::agent;
8use crate::agent::app_server::{RealCodexAppServerClient, RealGeminiAcpClient};
9use crate::app_server::{
10 AppServerClient, AppServerError, AppServerFuture, AppServerStreamEvent, AppServerTurnRequest,
11 AppServerTurnResponse,
12};
13use crate::model::agent::AgentKind;
14
15pub struct RoutingAppServerClient {
17 codex_client: Arc<dyn AppServerClient>,
18 gemini_client: Arc<dyn AppServerClient>,
19}
20
21impl RoutingAppServerClient {
22 pub fn new() -> Self {
24 Self::new_with_clients(
25 Arc::new(RealCodexAppServerClient::new()),
26 Arc::new(RealGeminiAcpClient::new()),
27 )
28 }
29
30 pub fn new_with_clients(
32 codex_client: Arc<dyn AppServerClient>,
33 gemini_client: Arc<dyn AppServerClient>,
34 ) -> Self {
35 Self {
36 codex_client,
37 gemini_client,
38 }
39 }
40}
41
42impl Default for RoutingAppServerClient {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl AppServerClient for RoutingAppServerClient {
49 fn run_turn(
50 &self,
51 request: AppServerTurnRequest,
52 stream_tx: mpsc::UnboundedSender<AppServerStreamEvent>,
53 ) -> AppServerFuture<Result<AppServerTurnResponse, AppServerError>> {
54 let codex_client = Arc::clone(&self.codex_client);
55 let gemini_client = Arc::clone(&self.gemini_client);
56 let model = request.model.clone();
57
58 Box::pin(async move {
59 let provider_kind = agent::provider_kind_for_model(&model).map_err(|error| {
60 AppServerError::Provider(format!("App-server routing failed for {error}"))
61 })?;
62
63 match provider_kind {
64 AgentKind::Codex => codex_client.run_turn(request, stream_tx).await,
65 AgentKind::Gemini => gemini_client.run_turn(request, stream_tx).await,
66 AgentKind::Antigravity => Err(AppServerError::Provider(
67 "Antigravity does not support app-server session execution".to_string(),
68 )),
69 AgentKind::Claude => Err(AppServerError::Provider(
70 "Claude does not support app-server session execution".to_string(),
71 )),
72 }
73 })
74 }
75
76 fn shutdown_session(&self, session_id: String) -> AppServerFuture<()> {
79 let codex_client = Arc::clone(&self.codex_client);
80 let gemini_client = Arc::clone(&self.gemini_client);
81
82 Box::pin(async move {
83 tokio::join!(
84 codex_client.shutdown_session(session_id.clone()),
85 gemini_client.shutdown_session(session_id),
86 );
87 })
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::app_server::MockAppServerClient;
95 use crate::model::agent::{AgentModel, ReasoningLevel};
96
97 #[tokio::test]
98 async fn run_turn_routes_codex_models_to_codex_client() {
99 let mut codex_client = MockAppServerClient::new();
101 codex_client.expect_run_turn().times(1).returning(|_, _| {
102 Box::pin(async {
103 Ok(AppServerTurnResponse {
104 assistant_message: "codex".to_string(),
105 context_reset: false,
106 input_tokens: 1,
107 output_tokens: 2,
108 pid: Some(111),
109 provider_conversation_id: Some("thread-codex".to_string()),
110 })
111 })
112 });
113 let mut gemini_client = MockAppServerClient::new();
114 gemini_client.expect_run_turn().times(0);
115 let app_server_client = RoutingAppServerClient::new_with_clients(
116 Arc::new(codex_client),
117 Arc::new(gemini_client),
118 );
119 let (stream_tx, _stream_rx) = mpsc::unbounded_channel();
120 let request = AppServerTurnRequest {
121 folder: std::env::temp_dir(),
122 live_transcript: None,
123 main_checkout_root: None,
124 model: AgentModel::Gpt55.as_str().to_string(),
125 prompt: "prompt".into(),
126 request_kind: crate::channel::AgentRequestKind::SessionStart,
127 replay_transcript: None,
128 provider_conversation_id: None,
129 persisted_instruction_conversation_id: None,
130 reasoning_level: ReasoningLevel::default(),
131 session_id: "session-1".to_string(),
132 };
133
134 let response = app_server_client
136 .run_turn(request, stream_tx)
137 .await
138 .expect("run_turn should succeed");
139
140 assert_eq!(response.assistant_message, "codex");
142 }
143
144 #[tokio::test]
145 async fn run_turn_routes_gemini_models_to_gemini_client() {
146 let mut codex_client = MockAppServerClient::new();
148 codex_client.expect_run_turn().times(0);
149 let mut gemini_client = MockAppServerClient::new();
150 gemini_client.expect_run_turn().times(1).returning(|_, _| {
151 Box::pin(async {
152 Ok(AppServerTurnResponse {
153 assistant_message: "gemini".to_string(),
154 context_reset: false,
155 input_tokens: 3,
156 output_tokens: 4,
157 pid: Some(222),
158 provider_conversation_id: Some("thread-gemini".to_string()),
159 })
160 })
161 });
162 let app_server_client = RoutingAppServerClient::new_with_clients(
163 Arc::new(codex_client),
164 Arc::new(gemini_client),
165 );
166 let (stream_tx, _stream_rx) = mpsc::unbounded_channel();
167 let request = AppServerTurnRequest {
168 folder: std::env::temp_dir(),
169 live_transcript: None,
170 main_checkout_root: None,
171 model: AgentModel::Gemini3FlashPreview.as_str().to_string(),
172 prompt: "prompt".into(),
173 request_kind: crate::channel::AgentRequestKind::SessionStart,
174 replay_transcript: None,
175 provider_conversation_id: None,
176 persisted_instruction_conversation_id: None,
177 reasoning_level: ReasoningLevel::default(),
178 session_id: "session-1".to_string(),
179 };
180
181 let response = app_server_client
183 .run_turn(request, stream_tx)
184 .await
185 .expect("run_turn should succeed");
186
187 assert_eq!(response.assistant_message, "gemini");
189 }
190
191 #[tokio::test]
192 async fn run_turn_returns_error_for_unknown_model() {
193 let mut codex_client = MockAppServerClient::new();
195 codex_client.expect_run_turn().times(0);
196 let mut gemini_client = MockAppServerClient::new();
197 gemini_client.expect_run_turn().times(0);
198 let app_server_client = RoutingAppServerClient::new_with_clients(
199 Arc::new(codex_client),
200 Arc::new(gemini_client),
201 );
202 let (stream_tx, _stream_rx) = mpsc::unbounded_channel();
203 let request = AppServerTurnRequest {
204 folder: std::env::temp_dir(),
205 live_transcript: None,
206 main_checkout_root: None,
207 model: "unknown-model".to_string(),
208 prompt: "prompt".into(),
209 request_kind: crate::channel::AgentRequestKind::SessionStart,
210 replay_transcript: None,
211 provider_conversation_id: None,
212 persisted_instruction_conversation_id: None,
213 reasoning_level: ReasoningLevel::default(),
214 session_id: "session-1".to_string(),
215 };
216
217 let result = app_server_client.run_turn(request, stream_tx).await;
219 let error = result.expect_err("should fail for unknown model");
220
221 assert!(
223 error.to_string().contains("unknown model"),
224 "error should mention unknown model"
225 );
226 }
227
228 #[tokio::test]
229 async fn shutdown_session_propagates_to_codex_client() {
230 let mut codex_client = MockAppServerClient::new();
232 codex_client
233 .expect_shutdown_session()
234 .times(1)
235 .returning(|_| Box::pin(async {}));
236 let mut gemini_client = MockAppServerClient::new();
237 gemini_client
238 .expect_shutdown_session()
239 .times(1)
240 .returning(|_| Box::pin(async {}));
241 let app_server_client = RoutingAppServerClient::new_with_clients(
242 Arc::new(codex_client),
243 Arc::new(gemini_client),
244 );
245
246 app_server_client
248 .shutdown_session("session-1".to_string())
249 .await;
250
251 }
253}