1use std::sync::Arc;
4
5#[cfg(test)]
6use ag_protocol::AgentResponseSummary;
7use ag_protocol::{
8 AgentResponse, ProtocolRequestProfile, ProtocolSchemaInstructionMode,
9 format_protocol_parse_debug_details, normalize_turn_response, parse_agent_response_strict,
10};
11
12use super::backend::{
13 AgentBackend, AgentBackendError, AgentPromptTransport, AgentTransport, AppServerThoughtPolicy,
14 BuildCommandRequest,
15};
16use super::prompt;
17use super::response_parser::ParsedResponse;
18use crate::app_server::AppServerClient;
19use crate::model::agent::AgentKind;
20
21type AppServerClientFactory =
24 fn(Option<Arc<dyn AppServerClient>>) -> Option<Arc<dyn AppServerClient>>;
25
26pub fn create_backend(kind: AgentKind) -> Box<dyn AgentBackend> {
28 (provider_descriptor(kind).backend_factory)()
29}
30
31pub fn create_app_server_client(
33 kind: AgentKind,
34 default_client: Option<Arc<dyn AppServerClient>>,
35) -> Option<Arc<dyn AppServerClient>> {
36 (provider_descriptor(kind).app_server_client_factory)(default_client)
37}
38
39pub(crate) fn parse_response(kind: AgentKind, stdout: &str, stderr: &str) -> ParsedResponse {
41 (provider_descriptor(kind).parse_response)(stdout, stderr)
42}
43
44pub(crate) fn parse_stream_output_line(
49 kind: AgentKind,
50 stdout_line: &str,
51) -> Option<(String, bool)> {
52 (provider_descriptor(kind).parse_stream_output_line)(stdout_line)
53}
54
55pub fn transport_mode(kind: AgentKind) -> AgentTransport {
57 provider_descriptor(kind).transport
58}
59
60pub(crate) fn prompt_transport(kind: AgentKind) -> AgentPromptTransport {
62 provider_descriptor(kind).prompt_transport
63}
64
65pub(crate) fn protocol_schema_instruction_mode(kind: AgentKind) -> ProtocolSchemaInstructionMode {
72 provider_descriptor(kind).protocol_schema_instruction_mode
73}
74
75pub(crate) fn parse_turn_response(
83 kind: AgentKind,
84 response_text: &str,
85 protocol_profile: ProtocolRequestProfile,
86) -> Result<AgentResponse, String> {
87 let response = parse_agent_response_strict(response_text).map_err(|error| {
88 format!(
89 "Agent output did not match the required JSON schema from {kind}: \
90 {error}\nprotocol_profile: \
91 {protocol_profile:?}\ndebug_details:\n{}\nresponse:\n{response_text}",
92 format_protocol_parse_debug_details(response_text)
93 )
94 })?;
95
96 Ok(normalize_turn_response(response, protocol_profile))
97}
98
99pub(crate) fn is_app_server_thought_chunk(
102 kind: AgentKind,
103 is_delta: bool,
104 phase: Option<&str>,
105) -> bool {
106 if !is_delta {
107 return false;
108 }
109
110 match provider_descriptor(kind).app_server_thought_policy {
111 AppServerThoughtPolicy::None => false,
112 AppServerThoughtPolicy::PhaseLabel => phase.is_some_and(is_codex_thought_phase_label),
113 }
114}
115
116pub(crate) fn build_command_stdin_payload(
122 kind: AgentKind,
123 request: BuildCommandRequest<'_>,
124) -> Result<Option<Vec<u8>>, AgentBackendError> {
125 let protocol_schema_instruction_mode = protocol_schema_instruction_mode(kind);
126
127 match prompt_transport(kind) {
128 AgentPromptTransport::Argv => Ok(None),
129 AgentPromptTransport::Stdin => match kind {
130 AgentKind::Antigravity => prompt::build_prompt_stdin_payload(
131 request,
132 protocol_schema_instruction_mode,
133 "Antigravity",
134 )
135 .map(Some),
136 AgentKind::Claude => prompt::build_prompt_stdin_payload(
137 request,
138 protocol_schema_instruction_mode,
139 "Claude",
140 )
141 .map(Some),
142 AgentKind::Codex | AgentKind::Gemini => Ok(None),
143 },
144 }
145}
146
147struct AgentProviderDescriptor {
149 app_server_client_factory: AppServerClientFactory,
150 app_server_thought_policy: AppServerThoughtPolicy,
151 backend_factory: fn() -> Box<dyn AgentBackend>,
152 parse_response: fn(&str, &str) -> ParsedResponse,
153 parse_stream_output_line: fn(&str) -> Option<(String, bool)>,
154 prompt_transport: AgentPromptTransport,
155 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode,
156 transport: AgentTransport,
157}
158
159fn provider_descriptor(kind: AgentKind) -> AgentProviderDescriptor {
160 match kind {
161 AgentKind::Antigravity => AgentProviderDescriptor {
162 app_server_client_factory: |_default_client| None,
163 app_server_thought_policy: AppServerThoughtPolicy::None,
164 backend_factory: || Box::new(super::antigravity::AntigravityBackend),
165 parse_response: super::response_parser::parse_antigravity_response_with_fallback,
166 parse_stream_output_line: super::response_parser::parse_antigravity_stream_output_line,
167 prompt_transport: AgentPromptTransport::Stdin,
168 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::PromptSchema,
169 transport: AgentTransport::Cli,
170 },
171 AgentKind::Gemini => AgentProviderDescriptor {
172 app_server_client_factory: |default_client| {
173 Some(default_client.unwrap_or_else(|| {
174 Arc::new(super::app_server::RealGeminiAcpClient::new())
175 as Arc<dyn AppServerClient>
176 }))
177 },
178 app_server_thought_policy: AppServerThoughtPolicy::None,
179 backend_factory: || Box::new(super::gemini::GeminiBackend),
180 parse_response: super::response_parser::parse_gemini_response_with_fallback,
181 parse_stream_output_line: super::response_parser::parse_gemini_stream_output_line,
182 prompt_transport: AgentPromptTransport::Argv,
183 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::PromptSchema,
184 transport: AgentTransport::AppServer,
185 },
186 AgentKind::Claude => AgentProviderDescriptor {
187 app_server_client_factory: |_default_client| None,
188 app_server_thought_policy: AppServerThoughtPolicy::None,
189 backend_factory: || Box::new(super::claude::ClaudeBackend),
190 parse_response: super::response_parser::parse_claude_response_with_fallback,
191 parse_stream_output_line: super::response_parser::parse_claude_stream_output_line,
192 prompt_transport: AgentPromptTransport::Stdin,
193 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::TransportSchema,
194 transport: AgentTransport::Cli,
195 },
196 AgentKind::Codex => AgentProviderDescriptor {
197 app_server_client_factory: |default_client| {
198 Some(default_client.unwrap_or_else(|| {
199 Arc::new(super::app_server::RealCodexAppServerClient::new())
200 as Arc<dyn AppServerClient>
201 }))
202 },
203 app_server_thought_policy: AppServerThoughtPolicy::PhaseLabel,
204 backend_factory: || Box::new(super::codex::CodexBackend),
205 parse_response: super::response_parser::parse_codex_response_with_fallback,
206 parse_stream_output_line: super::response_parser::parse_codex_stream_output_line,
207 prompt_transport: AgentPromptTransport::Argv,
208 protocol_schema_instruction_mode: ProtocolSchemaInstructionMode::TransportSchema,
209 transport: AgentTransport::AppServer,
210 },
211 }
212}
213
214fn is_codex_thought_phase_label(phase: &str) -> bool {
219 let normalized_phase = phase.trim();
220
221 normalized_phase.eq_ignore_ascii_case("thinking")
222 || normalized_phase.eq_ignore_ascii_case("plan")
223 || normalized_phase.eq_ignore_ascii_case("reasoning")
224 || normalized_phase.eq_ignore_ascii_case("thought")
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_transport_mode_reports_expected_transport_by_provider() {
235 let antigravity_kind = AgentKind::Antigravity;
237 let claude_kind = AgentKind::Claude;
238 let codex_kind = AgentKind::Codex;
239 let gemini_kind = AgentKind::Gemini;
240
241 let antigravity_transport = transport_mode(antigravity_kind);
243 let claude_transport = transport_mode(claude_kind);
244 let codex_transport = transport_mode(codex_kind);
245 let gemini_transport = transport_mode(gemini_kind);
246
247 assert_eq!(antigravity_transport, AgentTransport::Cli);
249 assert_eq!(claude_transport, AgentTransport::Cli);
250 assert_eq!(codex_transport, AgentTransport::AppServer);
251 assert_eq!(gemini_transport, AgentTransport::AppServer);
252 }
253
254 #[test]
255 fn test_prompt_transport_reports_expected_mode_by_provider() {
258 let antigravity_kind = AgentKind::Antigravity;
260 let claude_kind = AgentKind::Claude;
261 let codex_kind = AgentKind::Codex;
262 let gemini_kind = AgentKind::Gemini;
263
264 let antigravity_transport = prompt_transport(antigravity_kind);
266 let claude_transport = prompt_transport(claude_kind);
267 let codex_transport = prompt_transport(codex_kind);
268 let gemini_transport = prompt_transport(gemini_kind);
269
270 assert_eq!(antigravity_transport, AgentPromptTransport::Stdin);
272 assert_eq!(claude_transport, AgentPromptTransport::Stdin);
273 assert_eq!(codex_transport, AgentPromptTransport::Argv);
274 assert_eq!(gemini_transport, AgentPromptTransport::Argv);
275 }
276
277 #[test]
278 fn test_protocol_schema_instruction_mode_reports_expected_mode_by_provider() {
281 assert_eq!(
283 protocol_schema_instruction_mode(AgentKind::Antigravity),
284 ProtocolSchemaInstructionMode::PromptSchema
285 );
286 assert_eq!(
287 protocol_schema_instruction_mode(AgentKind::Gemini),
288 ProtocolSchemaInstructionMode::PromptSchema
289 );
290 assert_eq!(
291 protocol_schema_instruction_mode(AgentKind::Claude),
292 ProtocolSchemaInstructionMode::TransportSchema
293 );
294 assert_eq!(
295 protocol_schema_instruction_mode(AgentKind::Codex),
296 ProtocolSchemaInstructionMode::TransportSchema
297 );
298 }
299
300 #[test]
301 fn test_parse_turn_response_rejects_invalid_payload() {
303 let raw_response = "plain response";
305
306 for kind in [
307 AgentKind::Antigravity,
308 AgentKind::Claude,
309 AgentKind::Codex,
310 AgentKind::Gemini,
311 ] {
312 let error =
314 parse_turn_response(kind, raw_response, ProtocolRequestProfile::SessionTurn)
315 .expect_err("plain response should fail strict protocol parsing");
316
317 assert!(error.contains("debug_details:"));
319 assert!(error.contains("first_non_whitespace_char: 'p'"));
320 assert!(error.contains("direct_json_error_location: line 1, column 1"));
321 }
322 }
323
324 #[test]
325 fn test_parse_turn_response_fills_missing_summary_for_session_turn() {
328 let raw_response = r#"{"answer":"done","questions":[],"summary":null}"#;
330
331 let result = parse_turn_response(
333 AgentKind::Codex,
334 raw_response,
335 ProtocolRequestProfile::SessionTurn,
336 )
337 .expect("valid protocol response should parse");
338
339 assert_eq!(result.answer, "done");
341 assert_eq!(
342 result.summary,
343 Some(AgentResponseSummary {
344 session: String::new(),
345 turn: String::new(),
346 })
347 );
348 }
349
350 #[test]
351 fn test_is_app_server_thought_chunk_reports_codex_phase_labels() {
354 assert!(is_app_server_thought_chunk(
356 AgentKind::Codex,
357 true,
358 Some("thinking"),
359 ));
360 assert!(!is_app_server_thought_chunk(
361 AgentKind::Gemini,
362 true,
363 Some("thinking"),
364 ));
365 }
366}