1use std::path::Path;
4use std::sync::Arc;
5
6#[cfg(test)]
7use ag_protocol::AgentResponseSummary;
8use ag_protocol::{
9 AgentResponse, ProtocolRequestProfile, ProtocolSchemaInstructionMode,
10 format_protocol_parse_debug_details, normalize_turn_response, parse_agent_response_strict,
11};
12
13use super::backend::{
14 AgentBackend, AgentBackendError, AgentPromptTransport, AgentTransport, AppServerThoughtPolicy,
15 BuildCommandRequest,
16};
17use super::prompt;
18use super::response_parser::ParsedResponse;
19use crate::app_server::AppServerClient;
20use crate::model::agent::{AgentKind, AgentModel};
21
22type AppServerClientFactory =
25 fn(Option<Arc<dyn AppServerClient>>) -> Option<Arc<dyn AppServerClient>>;
26
27pub fn create_backend(kind: AgentKind) -> Box<dyn AgentBackend> {
29 (provider_descriptor(kind).backend_factory)()
30}
31
32pub fn create_app_server_client(
34 kind: AgentKind,
35 default_client: Option<Arc<dyn AppServerClient>>,
36) -> Option<Arc<dyn AppServerClient>> {
37 (provider_descriptor(kind).app_server_client_factory)(default_client)
38}
39
40pub fn parse_response(kind: AgentKind, stdout: &str, stderr: &str) -> ParsedResponse {
42 (provider_descriptor(kind).parse_response)(stdout, stderr)
43}
44
45pub fn cleanup_session_worktree_artifacts(folder: &Path) -> Result<(), AgentBackendError> {
56 super::antigravity::cleanup_workspace_alias(folder)
57}
58
59pub fn parse_stream_output_line(kind: AgentKind, stdout_line: &str) -> Option<(String, bool)> {
64 (provider_descriptor(kind).parse_stream_output_line)(stdout_line)
65}
66
67pub fn transport_mode(kind: AgentKind) -> AgentTransport {
69 provider_descriptor(kind).transport
70}
71
72pub(crate) fn prompt_transport(kind: AgentKind) -> AgentPromptTransport {
74 provider_descriptor(kind).prompt_transport
75}
76
77pub fn protocol_schema_instruction_mode(kind: AgentKind) -> ProtocolSchemaInstructionMode {
84 provider_descriptor(kind).protocol_schema_instruction_mode
85}
86
87pub fn parse_turn_response(
95 kind: AgentKind,
96 response_text: &str,
97 protocol_profile: ProtocolRequestProfile,
98) -> Result<AgentResponse, String> {
99 let response = parse_agent_response_strict(response_text).map_err(|error| {
100 format!(
101 "Agent output did not match the required JSON schema from {kind}: \
102 {error}\nprotocol_profile: \
103 {protocol_profile:?}\ndebug_details:\n{}\nresponse:\n{response_text}",
104 format_protocol_parse_debug_details(response_text)
105 )
106 })?;
107
108 Ok(normalize_turn_response(response, protocol_profile))
109}
110
111pub fn is_app_server_thought_chunk(kind: AgentKind, is_delta: bool, phase: Option<&str>) -> bool {
114 if !is_delta {
115 return false;
116 }
117
118 match provider_descriptor(kind).app_server_thought_policy {
119 AppServerThoughtPolicy::None => false,
120 AppServerThoughtPolicy::PhaseLabel => phase.is_some_and(is_codex_thought_phase_label),
121 }
122}
123
124pub fn build_command_stdin_payload(
130 kind: AgentKind,
131 request: BuildCommandRequest<'_>,
132) -> Result<Option<Vec<u8>>, AgentBackendError> {
133 let protocol_schema_instruction_mode = protocol_schema_instruction_mode(kind);
134
135 match prompt_transport(kind) {
136 AgentPromptTransport::Argv => Ok(None),
137 AgentPromptTransport::Stdin => match kind {
138 AgentKind::Antigravity => prompt::build_prompt_stdin_payload(
139 request,
140 protocol_schema_instruction_mode,
141 "Antigravity",
142 )
143 .map(Some),
144 AgentKind::Claude => prompt::build_prompt_stdin_payload(
145 request,
146 protocol_schema_instruction_mode,
147 "Claude",
148 )
149 .map(Some),
150 AgentKind::Codex | AgentKind::Gemini => Ok(None),
151 },
152 }
153}
154
155pub fn provider_kind_for_model(model: &str) -> Result<AgentKind, String> {
161 let model = model
162 .parse::<AgentModel>()
163 .map_err(|error| format!("unknown model `{model}`: {error}"))?;
164 if AgentKind::Codex.supports_model(model) {
165 return Ok(AgentKind::Codex);
166 }
167
168 if AgentKind::Gemini.supports_model(model) {
169 return Ok(AgentKind::Gemini);
170 }
171
172 Err(format!(
173 "model `{}` does not use app-server routing",
174 model.as_str()
175 ))
176}
177
178struct AgentProviderDescriptor {
180 app_server_client_factory: AppServerClientFactory,
181 app_server_thought_policy: AppServerThoughtPolicy,
182 backend_factory: fn() -> Box<dyn AgentBackend>,
183 parse_response: fn(&str, &str) -> ParsedResponse,
184 parse_stream_output_line: fn(&str) -> Option<(String, bool)>,
185 prompt_transport: AgentPromptTransport,
186 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode,
187 transport: AgentTransport,
188}
189
190fn provider_descriptor(kind: AgentKind) -> AgentProviderDescriptor {
191 match kind {
192 AgentKind::Antigravity => AgentProviderDescriptor {
193 app_server_client_factory: |_default_client| None,
194 app_server_thought_policy: AppServerThoughtPolicy::None,
195 backend_factory: || Box::new(super::antigravity::AntigravityBackend),
196 parse_response: super::response_parser::parse_antigravity_response_with_fallback,
197 parse_stream_output_line: super::response_parser::parse_antigravity_stream_output_line,
198 prompt_transport: AgentPromptTransport::Stdin,
199 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::PromptSchema,
200 transport: AgentTransport::Cli,
201 },
202 AgentKind::Gemini => AgentProviderDescriptor {
203 app_server_client_factory: |default_client| {
204 Some(default_client.unwrap_or_else(|| {
205 Arc::new(super::app_server::RealGeminiAcpClient::new())
206 as Arc<dyn AppServerClient>
207 }))
208 },
209 app_server_thought_policy: AppServerThoughtPolicy::None,
210 backend_factory: || Box::new(super::gemini::GeminiBackend),
211 parse_response: super::response_parser::parse_gemini_response_with_fallback,
212 parse_stream_output_line: super::response_parser::parse_gemini_stream_output_line,
213 prompt_transport: AgentPromptTransport::Argv,
214 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::PromptSchema,
215 transport: AgentTransport::AppServer,
216 },
217 AgentKind::Claude => AgentProviderDescriptor {
218 app_server_client_factory: |_default_client| None,
219 app_server_thought_policy: AppServerThoughtPolicy::None,
220 backend_factory: || Box::new(super::claude::ClaudeBackend),
221 parse_response: super::response_parser::parse_claude_response_with_fallback,
222 parse_stream_output_line: super::response_parser::parse_claude_stream_output_line,
223 prompt_transport: AgentPromptTransport::Stdin,
224 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::TransportSchema,
225 transport: AgentTransport::Cli,
226 },
227 AgentKind::Codex => AgentProviderDescriptor {
228 app_server_client_factory: |default_client| {
229 Some(default_client.unwrap_or_else(|| {
230 Arc::new(super::app_server::RealCodexAppServerClient::new())
231 as Arc<dyn AppServerClient>
232 }))
233 },
234 app_server_thought_policy: AppServerThoughtPolicy::PhaseLabel,
235 backend_factory: || Box::new(super::codex::CodexBackend),
236 parse_response: super::response_parser::parse_codex_response_with_fallback,
237 parse_stream_output_line: super::response_parser::parse_codex_stream_output_line,
238 prompt_transport: AgentPromptTransport::Argv,
239 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::TransportSchema,
240 transport: AgentTransport::AppServer,
241 },
242 }
243}
244
245fn is_codex_thought_phase_label(phase: &str) -> bool {
250 let normalized_phase = phase.trim();
251
252 normalized_phase.eq_ignore_ascii_case("thinking")
253 || normalized_phase.eq_ignore_ascii_case("plan")
254 || normalized_phase.eq_ignore_ascii_case("reasoning")
255 || normalized_phase.eq_ignore_ascii_case("thought")
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_transport_mode_reports_expected_transport_by_provider() {
266 let antigravity_kind = AgentKind::Antigravity;
268 let claude_kind = AgentKind::Claude;
269 let codex_kind = AgentKind::Codex;
270 let gemini_kind = AgentKind::Gemini;
271
272 let antigravity_transport = transport_mode(antigravity_kind);
274 let claude_transport = transport_mode(claude_kind);
275 let codex_transport = transport_mode(codex_kind);
276 let gemini_transport = transport_mode(gemini_kind);
277
278 assert_eq!(antigravity_transport, AgentTransport::Cli);
280 assert_eq!(claude_transport, AgentTransport::Cli);
281 assert_eq!(codex_transport, AgentTransport::AppServer);
282 assert_eq!(gemini_transport, AgentTransport::AppServer);
283 }
284
285 #[test]
286 fn test_prompt_transport_reports_expected_mode_by_provider() {
289 let antigravity_kind = AgentKind::Antigravity;
291 let claude_kind = AgentKind::Claude;
292 let codex_kind = AgentKind::Codex;
293 let gemini_kind = AgentKind::Gemini;
294
295 let antigravity_transport = prompt_transport(antigravity_kind);
297 let claude_transport = prompt_transport(claude_kind);
298 let codex_transport = prompt_transport(codex_kind);
299 let gemini_transport = prompt_transport(gemini_kind);
300
301 assert_eq!(antigravity_transport, AgentPromptTransport::Stdin);
303 assert_eq!(claude_transport, AgentPromptTransport::Stdin);
304 assert_eq!(codex_transport, AgentPromptTransport::Argv);
305 assert_eq!(gemini_transport, AgentPromptTransport::Argv);
306 }
307
308 #[test]
309 fn test_protocol_schema_instruction_mode_reports_expected_mode_by_provider() {
312 assert_eq!(
314 protocol_schema_instruction_mode(AgentKind::Antigravity),
315 ProtocolSchemaInstructionMode::PromptSchema
316 );
317 assert_eq!(
318 protocol_schema_instruction_mode(AgentKind::Gemini),
319 ProtocolSchemaInstructionMode::PromptSchema
320 );
321 assert_eq!(
322 protocol_schema_instruction_mode(AgentKind::Claude),
323 ProtocolSchemaInstructionMode::TransportSchema
324 );
325 assert_eq!(
326 protocol_schema_instruction_mode(AgentKind::Codex),
327 ProtocolSchemaInstructionMode::TransportSchema
328 );
329 }
330
331 #[test]
332 fn test_parse_turn_response_rejects_invalid_payload() {
334 let raw_response = "plain response";
336
337 for kind in [
338 AgentKind::Antigravity,
339 AgentKind::Claude,
340 AgentKind::Codex,
341 AgentKind::Gemini,
342 ] {
343 let error =
345 parse_turn_response(kind, raw_response, ProtocolRequestProfile::SessionTurn)
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 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(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}