diff --git a/code-rs/code-auto-drive-core/src/auto_coordinator.rs b/code-rs/code-auto-drive-core/src/auto_coordinator.rs index a9b785bbb5c..595b67960a3 100644 --- a/code-rs/code-auto-drive-core/src/auto_coordinator.rs +++ b/code-rs/code-auto-drive-core/src/auto_coordinator.rs @@ -2346,7 +2346,7 @@ fn agent_action_to_event(action: &AgentAction) -> AutoTurnAgentsAction { } } -pub(super) fn extract_first_json_object(input: &str) -> Option { +pub(crate) fn extract_first_json_object(input: &str) -> Option { let mut depth = 0usize; let mut in_str = false; let mut escape = false; diff --git a/code-rs/code-auto-drive-core/src/coordinator_user_schema.rs b/code-rs/code-auto-drive-core/src/coordinator_user_schema.rs index 2b1887a22b7..b3f63f392b3 100644 --- a/code-rs/code-auto-drive-core/src/coordinator_user_schema.rs +++ b/code-rs/code-auto-drive-core/src/coordinator_user_schema.rs @@ -3,6 +3,8 @@ use anyhow::Context; use serde_json::Value; +use crate::auto_coordinator::extract_first_json_object; + pub fn user_turn_schema() -> Value { serde_json::json!({ "type": "object", @@ -24,8 +26,20 @@ pub fn user_turn_schema() -> Value { } pub fn parse_user_turn_reply(raw: &str) -> anyhow::Result<(Option, Option)> { - let value: Value = serde_json::from_str(raw) - .context("parsing coordinator user turn JSON")?; + let value: Value = match serde_json::from_str(raw) { + Ok(v) => v, + Err(first_err) => { + let Some(blob) = extract_first_json_object(raw) else { + return Err(first_err).context("parsing coordinator user turn JSON"); + }; + let first_err_msg = first_err.to_string(); + serde_json::from_str(&blob).with_context(|| { + format!( + "parsing coordinator user turn JSON (after salvage); initial parse error: {first_err_msg}" + ) + })? + } + }; let obj = value .as_object() .ok_or_else(|| anyhow::anyhow!("coordinator response was not a JSON object"))?; @@ -52,3 +66,29 @@ pub fn parse_user_turn_reply(raw: &str) -> anyhow::Result<(Option, Optio Ok((extract("user_response")?, extract("cli_command")?)) } + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + + #[test] + fn parse_user_turn_reply_strict_object() -> Result<()> { + let raw = r#"{"user_response":" Thanks! ","cli_command":null}"#; + let (user, cli) = parse_user_turn_reply(raw)?; + assert_eq!(user.as_deref(), Some("Thanks!")); + assert_eq!(cli, None); + Ok(()) + } + + #[test] + fn parse_user_turn_reply_salvages_embedded_json() -> Result<()> { + let raw = r#"Here are two options: do A or B. +{"user_response":null,"cli_command":" echo done "} +Let me know."#; + let (user, cli) = parse_user_turn_reply(raw)?; + assert_eq!(user, None); + assert_eq!(cli.as_deref(), Some("echo done")); + Ok(()) + } +}