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_session_output: None,
123 model: AgentModel::Gpt55.as_str().to_string(),
124 prompt: "prompt".into(),
125 request_kind: crate::channel::AgentRequestKind::SessionStart,
126 provider_conversation_id: None,
127 persisted_instruction_conversation_id: None,
128 reasoning_level: ReasoningLevel::default(),
129 session_id: "session-1".to_string(),
130 };
131
132 let response = app_server_client
134 .run_turn(request, stream_tx)
135 .await
136 .expect("run_turn should succeed");
137
138 assert_eq!(response.assistant_message, "codex");
140 }
141
142 #[tokio::test]
143 async fn run_turn_routes_gemini_models_to_gemini_client() {
144 let mut codex_client = MockAppServerClient::new();
146 codex_client.expect_run_turn().times(0);
147 let mut gemini_client = MockAppServerClient::new();
148 gemini_client.expect_run_turn().times(1).returning(|_, _| {
149 Box::pin(async {
150 Ok(AppServerTurnResponse {
151 assistant_message: "gemini".to_string(),
152 context_reset: false,
153 input_tokens: 3,
154 output_tokens: 4,
155 pid: Some(222),
156 provider_conversation_id: Some("thread-gemini".to_string()),
157 })
158 })
159 });
160 let app_server_client = RoutingAppServerClient::new_with_clients(
161 Arc::new(codex_client),
162 Arc::new(gemini_client),
163 );
164 let (stream_tx, _stream_rx) = mpsc::unbounded_channel();
165 let request = AppServerTurnRequest {
166 folder: std::env::temp_dir(),
167 live_session_output: None,
168 model: AgentModel::Gemini3FlashPreview.as_str().to_string(),
169 prompt: "prompt".into(),
170 request_kind: crate::channel::AgentRequestKind::SessionStart,
171 provider_conversation_id: None,
172 persisted_instruction_conversation_id: None,
173 reasoning_level: ReasoningLevel::default(),
174 session_id: "session-1".to_string(),
175 };
176
177 let response = app_server_client
179 .run_turn(request, stream_tx)
180 .await
181 .expect("run_turn should succeed");
182
183 assert_eq!(response.assistant_message, "gemini");
185 }
186
187 #[tokio::test]
188 async fn run_turn_returns_error_for_unknown_model() {
189 let mut codex_client = MockAppServerClient::new();
191 codex_client.expect_run_turn().times(0);
192 let mut gemini_client = MockAppServerClient::new();
193 gemini_client.expect_run_turn().times(0);
194 let app_server_client = RoutingAppServerClient::new_with_clients(
195 Arc::new(codex_client),
196 Arc::new(gemini_client),
197 );
198 let (stream_tx, _stream_rx) = mpsc::unbounded_channel();
199 let request = AppServerTurnRequest {
200 folder: std::env::temp_dir(),
201 live_session_output: None,
202 model: "unknown-model".to_string(),
203 prompt: "prompt".into(),
204 request_kind: crate::channel::AgentRequestKind::SessionStart,
205 provider_conversation_id: None,
206 persisted_instruction_conversation_id: None,
207 reasoning_level: ReasoningLevel::default(),
208 session_id: "session-1".to_string(),
209 };
210
211 let result = app_server_client.run_turn(request, stream_tx).await;
213 let error = result.expect_err("should fail for unknown model");
214
215 assert!(
217 error.to_string().contains("unknown model"),
218 "error should mention unknown model"
219 );
220 }
221
222 #[tokio::test]
223 async fn shutdown_session_propagates_to_codex_client() {
224 let mut codex_client = MockAppServerClient::new();
226 codex_client
227 .expect_shutdown_session()
228 .times(1)
229 .returning(|_| Box::pin(async {}));
230 let mut gemini_client = MockAppServerClient::new();
231 gemini_client
232 .expect_shutdown_session()
233 .times(1)
234 .returning(|_| Box::pin(async {}));
235 let app_server_client = RoutingAppServerClient::new_with_clients(
236 Arc::new(codex_client),
237 Arc::new(gemini_client),
238 );
239
240 app_server_client
242 .shutdown_session("session-1".to_string())
243 .await;
244
245 }
247}