malvin 0.2.5

Non-interactive research and coding agent
use crate::acp::import_prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SessionUpdateChunkKind {
    Message,
    Thought,
}

pub(crate) const ACP_VERBOSE_COALESCE_MAX: usize = 125;

pub(crate) fn coalesce_append_chunk(
    buf: &mut String,
    buf_chars: &mut usize,
    chunk: &str,
    emissions: &mut Vec<String>,
) {
    let mut pos = 0usize;
    let b = chunk.as_bytes();
    while pos < b.len() {
        if let Some(rel) = b[pos..].iter().position(|&c| c == b'\n') {
            let end = pos + rel;
            let piece = &chunk[pos..end];
            buf.push_str(piece);
            *buf_chars += piece.chars().count();
            coalesce_flush_cap(buf, buf_chars, emissions);
            coalesce_flush_nonempty(buf, buf_chars, emissions);
            pos = end;
            while pos < b.len() && b[pos] == b'\n' {
                pos += 1;
            }
        } else {
            let piece = &chunk[pos..];
            buf.push_str(piece);
            *buf_chars += piece.chars().count();
            coalesce_flush_cap(buf, buf_chars, emissions);
            break;
        }
    }
}

pub(crate) fn coalesce_char_boundary_at(s: &str, n_chars: usize) -> usize {
    s.char_indices().nth(n_chars).map_or(s.len(), |(i, _)| i)
}

pub(crate) fn coalesce_flush_cap(
    buf: &mut String,
    buf_chars: &mut usize,
    emissions: &mut Vec<String>,
) {
    while *buf_chars >= ACP_VERBOSE_COALESCE_MAX {
        let hard_end = coalesce_char_boundary_at(buf, ACP_VERBOSE_COALESCE_MAX);
        let (emit_end, drain_end, drained_chars) = coalesce_word_split_points(buf, hard_end);
        let emitted = buf[..emit_end].to_string();
        buf.drain(..drain_end);
        *buf_chars -= drained_chars;
        emissions.push(emitted);
    }
}

fn coalesce_word_split_points(buf: &str, hard_end: usize) -> (usize, usize, usize) {
    let region = &buf[..hard_end];
    let mut last_space_start: Option<usize> = None;
    for (i, ch) in region.char_indices() {
        if ch == ' ' {
            last_space_start = Some(i);
        }
    }
    if let Some(space_start) = last_space_start {
        let mut drain_end = space_start;
        for ch in buf[space_start..hard_end].chars() {
            if ch == ' ' {
                drain_end += ch.len_utf8();
            } else {
                break;
            }
        }
        let emit_end = space_start;
        let emit_chars = buf[..emit_end].chars().count();
        if emit_chars > 0 {
            let drained_chars = buf[..drain_end].chars().count();
            return (emit_end, drain_end, drained_chars);
        }
    }
    (hard_end, hard_end, ACP_VERBOSE_COALESCE_MAX)
}

pub(crate) fn coalesce_flush_nonempty(
    buf: &mut String,
    buf_chars: &mut usize,
    emissions: &mut Vec<String>,
) {
    if !buf.is_empty() {
        emissions.push(std::mem::take(buf));
        *buf_chars = 0;
    }
}

#[derive(Default)]
#[allow(dead_code)]
pub(crate) struct VerboseIoCoalescer {
    pub message: String,
    pub thought: String,
    message_chars: usize,
    thought_chars: usize,
}

#[allow(dead_code)]
impl VerboseIoCoalescer {
    pub fn feed(&mut self, kind: SessionUpdateChunkKind, chunk: &str) {
        match kind {
            SessionUpdateChunkKind::Message => {
                Self::flush_if_nonempty(&mut self.thought, &mut self.thought_chars, "acp thought");
                Self::feed_buf(
                    &mut self.message,
                    &mut self.message_chars,
                    chunk,
                    "acp message",
                );
            }
            SessionUpdateChunkKind::Thought => {
                Self::flush_if_nonempty(&mut self.message, &mut self.message_chars, "acp message");
                Self::feed_buf(
                    &mut self.thought,
                    &mut self.thought_chars,
                    chunk,
                    "acp thought",
                );
            }
        }
    }

    pub fn flush_all(&mut self) {
        Self::flush_if_nonempty(&mut self.message, &mut self.message_chars, "acp message");
        Self::flush_if_nonempty(&mut self.thought, &mut self.thought_chars, "acp thought");
    }

    fn feed_buf(buf: &mut String, buf_chars: &mut usize, chunk: &str, label: &'static str) {
        let mut emissions = Vec::new();
        coalesce_append_chunk(buf, buf_chars, chunk, &mut emissions);
        for piece in emissions {
            info!(target: "malvin::acp::io", "{} {}", label, piece);
        }
    }

    fn flush_if_nonempty(buf: &mut String, buf_chars: &mut usize, label: &'static str) {
        if !buf.is_empty() {
            let piece = std::mem::take(buf);
            *buf_chars = 0;
            info!(target: "malvin::acp::io", "{} {}", label, piece);
        }
    }
}

#[cfg(test)]
mod coalesce_tests {
    use super::{
        ACP_VERBOSE_COALESCE_MAX, SessionUpdateChunkKind, VerboseIoCoalescer, coalesce_flush_cap,
    };

    #[test]
    fn feed_buf_and_flush_if_nonempty_fn_items() {
        let _ = (
            VerboseIoCoalescer::feed_buf,
            VerboseIoCoalescer::flush_if_nonempty,
        );
    }

    #[test]
    fn flush_if_nonempty_clears_on_kind_switch() {
        let mut c = VerboseIoCoalescer::default();
        c.feed(SessionUpdateChunkKind::Message, "hold");
        c.feed(SessionUpdateChunkKind::Thought, "think");
        assert!(c.message.is_empty());
        assert_eq!(c.thought, "think");
    }

    #[test]
    fn coalesce_flush_cap_preserves_tab_boundary_content() {
        let original = format!("{}\t{}", "a".repeat(50), "b".repeat(100));
        let mut buf = original.clone();
        let mut buf_chars = buf.chars().count();
        let mut emissions = Vec::new();
        coalesce_flush_cap(&mut buf, &mut buf_chars, &mut emissions);
        assert_eq!(emissions.len(), 1);
        assert_eq!(emissions[0].chars().count(), ACP_VERBOSE_COALESCE_MAX);
        let rebuilt = format!("{}{}", emissions.concat(), buf);
        assert_eq!(rebuilt, original);
        assert!(rebuilt.contains('\t'));
    }
}