use serde_json::Value;
pub fn parse_or_repair(raw: &str) -> Value {
if raw.trim().is_empty() {
return serde_json::json!({});
}
if let Ok(v) = serde_json::from_str::<Value>(raw) {
return v;
}
if let Some(repaired) = try_repair(raw)
&& let Ok(v) = serde_json::from_str::<Value>(&repaired)
{
tracing::warn!(
"[JSON_REPAIR] recovered partial args ({} bytes → {} bytes): {:?}",
raw.len(),
repaired.len(),
raw.chars()
.rev()
.take(80)
.collect::<String>()
.chars()
.rev()
.collect::<String>()
);
return v;
}
tracing::warn!(
"[JSON_REPAIR] FAILED to recover partial args ({} bytes): {:?}",
raw.len(),
raw.chars().take(200).collect::<String>()
);
serde_json::json!({
"_partial": raw,
"_repair_failed": true,
})
}
pub fn try_repair(raw: &str) -> Option<String> {
let mut chars: Vec<char> = raw.chars().collect();
while chars.last().is_some_and(|c| c.is_whitespace()) {
chars.pop();
}
if chars.is_empty() {
return None;
}
let mut in_string = false;
let mut escape = false;
let mut stack: Vec<char> = Vec::new();
let mut last_complete_value_end: Option<usize> = None;
let mut after_colon = false;
for (i, &c) in chars.iter().enumerate() {
if escape {
escape = false;
continue;
}
if c == '\\' && in_string {
escape = true;
continue;
}
if c == '"' {
in_string = !in_string;
if !in_string {
last_complete_value_end = Some(i);
after_colon = false; }
continue;
}
if in_string {
continue;
}
match c {
'{' | '[' => {
stack.push(c);
after_colon = false; }
'}' => {
if stack.last() == Some(&'{') {
stack.pop();
last_complete_value_end = Some(i);
after_colon = false;
} else {
return None; }
}
']' => {
if stack.last() == Some(&'[') {
stack.pop();
last_complete_value_end = Some(i);
after_colon = false;
} else {
return None;
}
}
':' => after_colon = true,
',' => after_colon = false,
c if !c.is_whitespace() => {
last_complete_value_end = Some(i);
after_colon = false;
}
_ => {}
}
}
let mut out: String = chars.iter().collect();
if in_string {
out.push('"');
}
if after_colon
&& !in_string
&& let Some(end) = last_complete_value_end
{
let bytes = out.as_bytes();
let mut cut = None;
for (i, &b) in bytes.iter().enumerate().take(end + 1).rev() {
if b == b',' {
cut = Some(i);
break;
}
if b == b'{' {
cut = Some(i + 1);
break;
}
}
if let Some(c) = cut {
out.truncate(c);
if out.ends_with(',') {
out.pop();
}
}
}
let trimmed = out.trim_end();
if let Some(stripped) = trimmed.strip_suffix(',') {
out = stripped.to_string();
}
while let Some(open) = stack.pop() {
match open {
'{' => out.push('}'),
'[' => out.push(']'),
_ => {}
}
}
Some(out)
}
use super::types::ContentBlock;
fn fenced_code_ranges(text: &str) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let mut in_fence = false;
let mut fence_start = 0usize;
let mut offset = 0usize;
for line in text.split_inclusive('\n') {
if line.trim_start().starts_with("```") {
if in_fence {
ranges.push((fence_start, offset + line.len()));
in_fence = false;
} else {
in_fence = true;
fence_start = offset;
}
}
offset += line.len();
}
if in_fence {
ranges.push((fence_start, offset));
}
ranges
}
fn is_call_shaped_json(candidate: &str) -> bool {
let Ok(v) = serde_json::from_str::<serde_json::Value>(candidate) else {
return false;
};
let Some(obj) = v.as_object() else {
return false;
};
if obj.get("name").is_some_and(|n| n.is_string()) && obj.contains_key("arguments") {
return true;
}
if obj.contains_key("command") {
return true;
}
if let Some(f) = obj.get("function")
&& f.get("name").is_some_and(|n| n.is_string())
&& f.get("arguments").is_some()
{
return true;
}
false
}
pub fn find_call_shaped_json_spans(text: &str) -> Vec<(usize, usize)> {
let fenced = fenced_code_ranges(text);
let bytes = text.as_bytes();
let mut spans = Vec::new();
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] == b'{' {
let mut depth = 0i32;
let mut in_string = false;
let mut escape = false;
let mut j = i;
let mut closed = false;
while j < bytes.len() {
let b = bytes[j];
if in_string {
if escape {
escape = false;
} else if b == b'\\' {
escape = true;
} else if b == b'"' {
in_string = false;
}
} else {
match b {
b'"' => in_string = true,
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
closed = true;
break;
}
}
_ => {}
}
}
j += 1;
}
if closed {
let end = j + 1;
let in_fence = fenced.iter().any(|(s, e)| i >= *s && j < *e);
if !in_fence && is_call_shaped_json(&text[i..end]) {
spans.push((i, end));
i = end; continue;
}
}
}
i += 1;
}
spans
}
pub fn strip_call_shaped_json(text: &str) -> (String, bool) {
let mut spans = find_call_shaped_json_spans(text);
if spans.is_empty() {
return (text.to_string(), false);
}
spans.sort_unstable();
let mut out = String::with_capacity(text.len());
let mut last_end = 0usize;
for (s, e) in spans {
if s < last_end {
continue;
}
out.push_str(&text[last_end..s]);
last_end = e;
}
out.push_str(&text[last_end..]);
(out.trim().to_string(), true)
}
pub fn content_has_unrecovered_tool_text(blocks: &[ContentBlock]) -> bool {
if blocks
.iter()
.any(|b| matches!(b, ContentBlock::ToolUse { .. }))
{
return false;
}
blocks.iter().any(|b| match b {
ContentBlock::Text { text } => !find_call_shaped_json_spans(text).is_empty(),
_ => false,
})
}