Skip to main content

codetether_agent/search/
parse.rs

1//! JSON parser for the router LLM response.
2//!
3//! Accepts raw text, strips any accidental fencing, and extracts the
4//! first JSON object we can parse into a [`RouterPlan`].
5
6use anyhow::{Context, Result, anyhow};
7use serde::Deserialize;
8
9use super::types::BackendChoice;
10
11/// Parsed plan returned by the router model.
12#[derive(Debug, Clone, Deserialize)]
13pub struct RouterPlan {
14    pub choices: Vec<BackendChoice>,
15}
16
17/// Parse a router response string into a [`RouterPlan`].
18///
19/// # Errors
20///
21/// Returns an error when no JSON object is present or the payload fails
22/// to match the expected schema.
23///
24/// # Examples
25///
26/// ```rust
27/// use codetether_agent::search::parse::parse_router_response;
28/// let plan = parse_router_response(
29///     r#"```json
30///     {"choices":[{"backend":"grep","args":{"pattern":"fn main"}}]}
31///     ```"#,
32/// )
33/// .unwrap();
34/// assert_eq!(plan.choices.len(), 1);
35/// ```
36pub fn parse_router_response(raw: &str) -> Result<RouterPlan> {
37    let trimmed = raw.trim();
38    if let Ok(plan) = serde_json::from_str::<RouterPlan>(trimmed) {
39        return Ok(plan);
40    }
41    let (start, end) = (
42        trimmed.find('{').ok_or_else(|| anyhow!("no '{{' found"))?,
43        trimmed.rfind('}').ok_or_else(|| anyhow!("no '}}' found"))?,
44    );
45    serde_json::from_str(&trimmed[start..=end]).context("router response is not valid RouterPlan")
46}