1use std::path::Path;
4use std::sync::Arc;
5
6use super::backend::{
7 AgentBackend, AgentBackendError, AgentPromptTransport, AgentTransport, AppServerThoughtPolicy,
8 BuildCommandRequest,
9};
10use super::prompt::{self, ProtocolSchemaInstructionMode};
11use super::protocol;
12use super::response_parser::ParsedResponse;
13use crate::app_server::AppServerClient;
14use crate::model::agent::{AgentKind, AgentModel};
15
16type AppServerClientFactory =
19 fn(Option<Arc<dyn AppServerClient>>) -> Option<Arc<dyn AppServerClient>>;
20
21pub fn create_backend(kind: AgentKind) -> Box<dyn AgentBackend> {
23 (provider_descriptor(kind).backend_factory)()
24}
25
26pub fn create_app_server_client(
28 kind: AgentKind,
29 default_client: Option<Arc<dyn AppServerClient>>,
30) -> Option<Arc<dyn AppServerClient>> {
31 (provider_descriptor(kind).app_server_client_factory)(default_client)
32}
33
34pub fn parse_response(kind: AgentKind, stdout: &str, stderr: &str) -> ParsedResponse {
36 (provider_descriptor(kind).parse_response)(stdout, stderr)
37}
38
39pub fn cleanup_session_worktree_artifacts(folder: &Path) -> Result<(), AgentBackendError> {
50 super::antigravity::cleanup_workspace_alias(folder)
51}
52
53pub fn parse_stream_output_line(kind: AgentKind, stdout_line: &str) -> Option<(String, bool)> {
58 (provider_descriptor(kind).parse_stream_output_line)(stdout_line)
59}
60
61pub fn transport_mode(kind: AgentKind) -> AgentTransport {
63 provider_descriptor(kind).transport
64}
65
66pub(crate) fn prompt_transport(kind: AgentKind) -> AgentPromptTransport {
68 provider_descriptor(kind).prompt_transport
69}
70
71pub fn protocol_schema_instruction_mode(kind: AgentKind) -> ProtocolSchemaInstructionMode {
78 provider_descriptor(kind).protocol_schema_instruction_mode
79}
80
81pub fn parse_turn_response(
89 kind: AgentKind,
90 response_text: &str,
91 protocol_profile: protocol::ProtocolRequestProfile,
92) -> Result<protocol::AgentResponse, String> {
93 let response = protocol::parse_agent_response_strict(response_text).map_err(|error| {
94 format!(
95 "Agent output did not match the required JSON schema from {kind}: \
96 {error}\nprotocol_profile: \
97 {protocol_profile:?}\ndebug_details:\n{}\nresponse:\n{response_text}",
98 protocol::format_protocol_parse_debug_details(response_text)
99 )
100 })?;
101
102 Ok(protocol::normalize_turn_response(
103 response,
104 protocol_profile,
105 ))
106}
107
108pub fn is_app_server_thought_chunk(kind: AgentKind, is_delta: bool, phase: Option<&str>) -> bool {
111 if !is_delta {
112 return false;
113 }
114
115 match provider_descriptor(kind).app_server_thought_policy {
116 AppServerThoughtPolicy::None => false,
117 AppServerThoughtPolicy::PhaseLabel => phase.is_some_and(is_codex_thought_phase_label),
118 }
119}
120
121pub fn build_command_stdin_payload(
127 kind: AgentKind,
128 request: BuildCommandRequest<'_>,
129) -> Result<Option<Vec<u8>>, AgentBackendError> {
130 let protocol_schema_instruction_mode = protocol_schema_instruction_mode(kind);
131
132 match prompt_transport(kind) {
133 AgentPromptTransport::Argv => Ok(None),
134 AgentPromptTransport::Stdin => match kind {
135 AgentKind::Antigravity => prompt::build_prompt_stdin_payload(
136 request,
137 protocol_schema_instruction_mode,
138 "Antigravity",
139 )
140 .map(Some),
141 AgentKind::Claude => prompt::build_prompt_stdin_payload(
142 request,
143 protocol_schema_instruction_mode,
144 "Claude",
145 )
146 .map(Some),
147 AgentKind::Codex | AgentKind::Gemini => Ok(None),
148 },
149 }
150}
151
152pub fn provider_kind_for_model(model: &str) -> Result<AgentKind, String> {
158 let model = model
159 .parse::<AgentModel>()
160 .map_err(|error| format!("unknown model `{model}`: {error}"))?;
161 if AgentKind::Codex.supports_model(model) {
162 return Ok(AgentKind::Codex);
163 }
164
165 if AgentKind::Gemini.supports_model(model) {
166 return Ok(AgentKind::Gemini);
167 }
168
169 Err(format!(
170 "model `{}` does not use app-server routing",
171 model.as_str()
172 ))
173}
174
175struct AgentProviderDescriptor {
177 app_server_client_factory: AppServerClientFactory,
178 app_server_thought_policy: AppServerThoughtPolicy,
179 backend_factory: fn() -> Box<dyn AgentBackend>,
180 parse_response: fn(&str, &str) -> ParsedResponse,
181 parse_stream_output_line: fn(&str) -> Option<(String, bool)>,
182 prompt_transport: AgentPromptTransport,
183 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode,
184 transport: AgentTransport,
185}
186
187fn provider_descriptor(kind: AgentKind) -> AgentProviderDescriptor {
188 match kind {
189 AgentKind::Antigravity => AgentProviderDescriptor {
190 app_server_client_factory: |_default_client| None,
191 app_server_thought_policy: AppServerThoughtPolicy::None,
192 backend_factory: || Box::new(super::antigravity::AntigravityBackend),
193 parse_response: super::response_parser::parse_antigravity_response_with_fallback,
194 parse_stream_output_line: super::response_parser::parse_antigravity_stream_output_line,
195 prompt_transport: AgentPromptTransport::Stdin,
196 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::PromptSchema,
197 transport: AgentTransport::Cli,
198 },
199 AgentKind::Gemini => AgentProviderDescriptor {
200 app_server_client_factory: |default_client| {
201 Some(default_client.unwrap_or_else(|| {
202 Arc::new(super::app_server::RealGeminiAcpClient::new())
203 as Arc<dyn AppServerClient>
204 }))
205 },
206 app_server_thought_policy: AppServerThoughtPolicy::None,
207 backend_factory: || Box::new(super::gemini::GeminiBackend),
208 parse_response: super::response_parser::parse_gemini_response_with_fallback,
209 parse_stream_output_line: super::response_parser::parse_gemini_stream_output_line,
210 prompt_transport: AgentPromptTransport::Argv,
211 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::PromptSchema,
212 transport: AgentTransport::AppServer,
213 },
214 AgentKind::Claude => AgentProviderDescriptor {
215 app_server_client_factory: |_default_client| None,
216 app_server_thought_policy: AppServerThoughtPolicy::None,
217 backend_factory: || Box::new(super::claude::ClaudeBackend),
218 parse_response: super::response_parser::parse_claude_response_with_fallback,
219 parse_stream_output_line: super::response_parser::parse_claude_stream_output_line,
220 prompt_transport: AgentPromptTransport::Stdin,
221 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::TransportSchema,
222 transport: AgentTransport::Cli,
223 },
224 AgentKind::Codex => AgentProviderDescriptor {
225 app_server_client_factory: |default_client| {
226 Some(default_client.unwrap_or_else(|| {
227 Arc::new(super::app_server::RealCodexAppServerClient::new())
228 as Arc<dyn AppServerClient>
229 }))
230 },
231 app_server_thought_policy: AppServerThoughtPolicy::PhaseLabel,
232 backend_factory: || Box::new(super::codex::CodexBackend),
233 parse_response: super::response_parser::parse_codex_response_with_fallback,
234 parse_stream_output_line: super::response_parser::parse_codex_stream_output_line,
235 prompt_transport: AgentPromptTransport::Argv,
236 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::TransportSchema,
237 transport: AgentTransport::AppServer,
238 },
239 }
240}
241
242fn is_codex_thought_phase_label(phase: &str) -> bool {
247 let normalized_phase = phase.trim();
248
249 normalized_phase.eq_ignore_ascii_case("thinking")
250 || normalized_phase.eq_ignore_ascii_case("plan")
251 || normalized_phase.eq_ignore_ascii_case("reasoning")
252 || normalized_phase.eq_ignore_ascii_case("thought")
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_transport_mode_reports_expected_transport_by_provider() {
263 let antigravity_kind = AgentKind::Antigravity;
265 let claude_kind = AgentKind::Claude;
266 let codex_kind = AgentKind::Codex;
267 let gemini_kind = AgentKind::Gemini;
268
269 let antigravity_transport = transport_mode(antigravity_kind);
271 let claude_transport = transport_mode(claude_kind);
272 let codex_transport = transport_mode(codex_kind);
273 let gemini_transport = transport_mode(gemini_kind);
274
275 assert_eq!(antigravity_transport, AgentTransport::Cli);
277 assert_eq!(claude_transport, AgentTransport::Cli);
278 assert_eq!(codex_transport, AgentTransport::AppServer);
279 assert_eq!(gemini_transport, AgentTransport::AppServer);
280 }
281
282 #[test]
283 fn test_prompt_transport_reports_expected_mode_by_provider() {
286 let antigravity_kind = AgentKind::Antigravity;
288 let claude_kind = AgentKind::Claude;
289 let codex_kind = AgentKind::Codex;
290 let gemini_kind = AgentKind::Gemini;
291
292 let antigravity_transport = prompt_transport(antigravity_kind);
294 let claude_transport = prompt_transport(claude_kind);
295 let codex_transport = prompt_transport(codex_kind);
296 let gemini_transport = prompt_transport(gemini_kind);
297
298 assert_eq!(antigravity_transport, AgentPromptTransport::Stdin);
300 assert_eq!(claude_transport, AgentPromptTransport::Stdin);
301 assert_eq!(codex_transport, AgentPromptTransport::Argv);
302 assert_eq!(gemini_transport, AgentPromptTransport::Argv);
303 }
304
305 #[test]
306 fn test_protocol_schema_instruction_mode_reports_expected_mode_by_provider() {
309 assert_eq!(
311 protocol_schema_instruction_mode(AgentKind::Antigravity),
312 ProtocolSchemaInstructionMode::PromptSchema
313 );
314 assert_eq!(
315 protocol_schema_instruction_mode(AgentKind::Gemini),
316 ProtocolSchemaInstructionMode::PromptSchema
317 );
318 assert_eq!(
319 protocol_schema_instruction_mode(AgentKind::Claude),
320 ProtocolSchemaInstructionMode::TransportSchema
321 );
322 assert_eq!(
323 protocol_schema_instruction_mode(AgentKind::Codex),
324 ProtocolSchemaInstructionMode::TransportSchema
325 );
326 }
327
328 #[test]
329 fn test_parse_turn_response_rejects_invalid_payload() {
331 let raw_response = "plain response";
333
334 for kind in [
335 AgentKind::Antigravity,
336 AgentKind::Claude,
337 AgentKind::Codex,
338 AgentKind::Gemini,
339 ] {
340 let error = parse_turn_response(
342 kind,
343 raw_response,
344 protocol::ProtocolRequestProfile::SessionTurn,
345 )
346 .expect_err("plain response should fail strict protocol parsing");
347
348 assert!(error.contains("debug_details:"));
350 assert!(error.contains("first_non_whitespace_char: 'p'"));
351 assert!(error.contains("direct_json_error_location: line 1, column 1"));
352 }
353 }
354
355 #[test]
356 fn test_parse_turn_response_fills_missing_summary_for_session_turn() {
359 let raw_response = r#"{"answer":"done","questions":[],"summary":null}"#;
361
362 let result = parse_turn_response(
364 AgentKind::Codex,
365 raw_response,
366 protocol::ProtocolRequestProfile::SessionTurn,
367 )
368 .expect("valid protocol response should parse");
369
370 assert_eq!(result.answer, "done");
372 assert_eq!(
373 result.summary,
374 Some(protocol::AgentResponseSummary {
375 session: String::new(),
376 turn: String::new(),
377 })
378 );
379 }
380
381 #[test]
382 fn test_is_app_server_thought_chunk_reports_codex_phase_labels() {
385 assert!(is_app_server_thought_chunk(
387 AgentKind::Codex,
388 true,
389 Some("thinking"),
390 ));
391 assert!(!is_app_server_thought_chunk(
392 AgentKind::Gemini,
393 true,
394 Some("thinking"),
395 ));
396 }
397
398 #[test]
399 fn test_provider_kind_for_model_reports_owner() {
401 assert_eq!(
403 provider_kind_for_model(AgentModel::Gemini31ProPreview.as_str()).expect("known model"),
404 AgentKind::Gemini
405 );
406 assert_eq!(
407 provider_kind_for_model(AgentModel::Gpt55.as_str()).expect("known model"),
408 AgentKind::Codex
409 );
410 }
411}