lucy-cli 1.14.5

A small local JSONL agent harness
Documentation
use serde_json::{json, Value};

use crate::context_budget::{usable_context, COMPACTION_OUTPUT_RESERVE_TOKENS};
use crate::model::{estimate_context_tokens, ChatMessage};

const HISTORY_OPEN: &str = "<discarded_history_json>\n";
const HISTORY_CLOSE: &str = "\n</discarded_history_json>";

pub(crate) fn summary_attempts(
    planned: Vec<ChatMessage>,
    context_window: Option<usize>,
) -> Result<Vec<Vec<ChatMessage>>, String> {
    let payload = planned
        .last()
        .and_then(|message| message.content.as_deref())
        .ok_or_else(|| "compaction plan has no transcript payload".to_owned())?;
    let (_, history) = payload
        .split_once(HISTORY_OPEN)
        .ok_or_else(|| "compaction plan has no discarded history".to_owned())?;
    let (history, _) = history
        .split_once(HISTORY_CLOSE)
        .ok_or_else(|| "compaction plan has an unterminated discarded history".to_owned())?;
    let records: Vec<Value> = serde_json::from_str(history)
        .map_err(|error| format!("invalid discarded history in compaction plan: {error}"))?;
    if records.is_empty() {
        return Err("compaction plan has empty discarded history".to_owned());
    }

    let mut candidates = vec![planned.clone()];
    candidates.push(rewrite_attempt(&planned, &records, records.len(), 500)?);
    candidates.push(rewrite_attempt(
        &planned,
        &records,
        records.len().div_ceil(2),
        500,
    )?);
    candidates.push(rewrite_attempt(
        &planned,
        &records,
        records.len().div_ceil(4),
        250,
    )?);
    candidates.dedup_by(|left, right| left == right);

    if let Some(window) = context_window {
        let usable = usable_context(window, COMPACTION_OUTPUT_RESERVE_TOKENS);
        let mut fitting = candidates
            .iter()
            .filter(|attempt| estimate_context_tokens(attempt) <= usable)
            .cloned()
            .collect::<Vec<_>>();
        if fitting.is_empty() {
            if let Some(smallest) = candidates.last().cloned() {
                fitting.push(smallest);
            }
        }
        return Ok(fitting);
    }
    Ok(candidates)
}

fn rewrite_attempt(
    planned: &[ChatMessage],
    records: &[Value],
    keep_latest: usize,
    max_string_chars: usize,
) -> Result<Vec<ChatMessage>, String> {
    let keep_latest = keep_latest.max(1).min(records.len());
    let omitted = records.len() - keep_latest;
    let mut reduced = records[omitted..].to_vec();
    for record in &mut reduced {
        truncate_json(record, max_string_chars);
    }
    if omitted > 0 {
        reduced.insert(
            0,
            json!({
                "record": "omitted_oldest_history",
                "count": omitted,
                "reason": "compaction input exceeded the reserved context budget"
            }),
        );
    }

    let mut attempt = planned.to_vec();
    let payload = attempt
        .last_mut()
        .and_then(|message| message.content.as_mut())
        .ok_or_else(|| "compaction plan has no mutable transcript payload".to_owned())?;
    let (prefix, history_and_suffix) = payload
        .split_once(HISTORY_OPEN)
        .ok_or_else(|| "compaction plan has no discarded history".to_owned())?;
    let (_, suffix) = history_and_suffix
        .split_once(HISTORY_CLOSE)
        .ok_or_else(|| "compaction plan has an unterminated discarded history".to_owned())?;
    let encoded = serde_json::to_string(&reduced)
        .map_err(|error| format!("unable to encode reduced compaction history: {error}"))?;
    *payload = format!("{prefix}{HISTORY_OPEN}{encoded}{HISTORY_CLOSE}{suffix}");
    Ok(attempt)
}

fn truncate_json(value: &mut Value, max_string_chars: usize) {
    match value {
        Value::String(text) => {
            let mut characters = text.chars();
            let prefix = characters
                .by_ref()
                .take(max_string_chars)
                .collect::<String>();
            if characters.next().is_some() {
                *text = format!(
                    "{prefix}\n[truncated for compaction fallback; field exceeded {max_string_chars} characters]"
                );
            }
        }
        Value::Array(values) => {
            for value in values {
                truncate_json(value, max_string_chars);
            }
        }
        Value::Object(values) => {
            for value in values.values_mut() {
                truncate_json(value, max_string_chars);
            }
        }
        Value::Null | Value::Bool(_) | Value::Number(_) => {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn planned(records: Vec<Value>) -> Vec<ChatMessage> {
        vec![
            ChatMessage::system("boot".to_owned()),
            ChatMessage::system("summarize".to_owned()),
            ChatMessage::user(format!(
                "<previous_summary>\nold\n</previous_summary>\n\n{HISTORY_OPEN}{}{HISTORY_CLOSE}",
                serde_json::to_string(&records).expect("records")
            )),
        ]
    }

    #[test]
    fn progressively_reduces_old_history() {
        let attempts = summary_attempts(
            planned(vec![
                json!({"content": "one"}),
                json!({"content": "two"}),
                json!({"content": "three"}),
                json!({"content": "four"}),
            ]),
            None,
        )
        .expect("attempts");
        assert_eq!(attempts.len(), 3);
        let smallest = attempts.last().expect("smallest")[2]
            .content
            .as_deref()
            .expect("payload");
        assert!(smallest.contains("omitted_oldest_history"));
        assert!(smallest.contains("four"));
        assert!(!smallest.contains("one"));
    }

    #[test]
    fn uses_the_smallest_attempt_when_none_fit_preflight() {
        let attempts = summary_attempts(
            planned(vec![json!({"content": "x".repeat(8_000)})]),
            Some(1_000),
        )
        .expect("attempts");
        assert_eq!(attempts.len(), 1);
        let payload = attempts[0][2].content.as_deref().expect("payload");
        assert!(payload.contains("truncated for compaction fallback"));
    }
}