use crate::llm::api::DeltaSender;
#[derive(Default)]
pub(super) struct DeltaCanonicalizer {
buf: String,
}
impl DeltaCanonicalizer {
pub(super) fn push(&mut self, chunk: &str) -> String {
use crate::llm::tool_delimiter::{
wire_to_canonical, WIRE_TOOL_CALL_CLOSE, WIRE_TOOL_CALL_OPEN,
};
self.buf.push_str(chunk);
let max = WIRE_TOOL_CALL_OPEN.len().max(WIRE_TOOL_CALL_CLOSE.len());
let blen = self.buf.len();
let mut hold = 0;
for k in (1..=max.min(blen)).rev() {
let tail = &self.buf.as_bytes()[blen - k..];
if WIRE_TOOL_CALL_OPEN.as_bytes().starts_with(tail)
|| WIRE_TOOL_CALL_CLOSE.as_bytes().starts_with(tail)
{
hold = k;
break;
}
}
let safe_end = blen - hold;
if safe_end == 0 {
return String::new();
}
let emit = wire_to_canonical(&self.buf[..safe_end]);
self.buf.drain(..safe_end);
emit
}
pub(super) fn flush(&mut self) -> String {
if self.buf.is_empty() {
return String::new();
}
let out = crate::llm::tool_delimiter::wire_to_canonical(&self.buf);
self.buf.clear();
out
}
}
pub(super) fn canonicalizing_delta_tx(orig: DeltaSender) -> DeltaSender {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
tokio::spawn(async move {
let mut canon = DeltaCanonicalizer::default();
while let Some(chunk) = rx.recv().await {
let emit = canon.push(&chunk);
if !emit.is_empty() {
let _ = orig.send(emit);
}
}
let tail = canon.flush();
if !tail.is_empty() {
let _ = orig.send(tail);
}
});
tx
}