use crate::providers::error::{ProviderError, ToolArgumentWhitespaceRunaway};
const MAX_JSON_WHITESPACE_RUN_BYTES: usize = 4 * 1024;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(in crate::providers) struct ToolArgumentWhitespaceState {
in_string: bool,
escaped: bool,
whitespace_run: usize,
}
impl ToolArgumentWhitespaceState {
pub(in crate::providers) fn observe(
&mut self,
delta: &str,
previous_bytes: usize,
tool_name: Option<&str>,
) -> anyhow::Result<bool> {
let mut semantic_progress = false;
for (offset, byte) in delta.bytes().enumerate() {
if self.in_string {
semantic_progress = true;
if self.escaped {
self.escaped = false;
} else if byte == b'\\' {
self.escaped = true;
} else if byte == b'"' {
self.in_string = false;
}
} else if matches!(byte, b' ' | b'\t' | b'\n' | b'\r') {
self.whitespace_run += 1;
if self.whitespace_run > MAX_JSON_WHITESPACE_RUN_BYTES {
let runaway = ToolArgumentWhitespaceRunaway {
tool_name: crate::output::redact_sensitive_text(
tool_name.unwrap_or("unknown"),
)
.chars()
.take(256)
.collect(),
argument_bytes: previous_bytes + offset + 1,
whitespace_run_bytes: self.whitespace_run,
};
return Err(anyhow::Error::new(runaway).context(
ProviderError::stream_terminal(
"provider tool call arguments degenerated into whitespace",
),
));
}
} else {
self.whitespace_run = 0;
self.in_string = byte == b'"';
semantic_progress = true;
}
}
Ok(semantic_progress)
}
}