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}