vv-agent 0.4.8

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::panic::{catch_unwind, AssertUnwindSafe};

use crate::memory::artifacts::{
    compact_tool_results, render_persisted_artifacts_section, ToolResultArtifactConfig,
};
use crate::memory::post_compact_restore::{restore_key_files, PostCompactRestoreConfig};
use crate::memory::summary::LocalSummary;
use crate::types::{Message, MessageRole};

use super::helpers::{extract_original_user_request, normalize_summary_output};
use super::normalization;
use super::prompts;
use super::MemoryManager;

impl MemoryManager {
    pub(super) fn compact_large_tool_results(
        &self,
        messages: &[Message],
        cycle_index: Option<u32>,
    ) -> (Vec<Message>, bool) {
        let (compacted, _artifacts, changed) = compact_tool_results(
            messages,
            &ToolResultArtifactConfig {
                workspace: self.config.workspace.clone(),
                artifact_dir: self.config.tool_result_artifact_dir.clone(),
                compact_threshold: self.config.tool_result_compact_threshold,
                keep_last: self.config.tool_result_keep_last,
                excerpt_head: self.config.tool_result_excerpt_head,
                excerpt_tail: self.config.tool_result_excerpt_tail,
            },
            cycle_index,
        );
        (compacted, changed)
    }

    pub(super) fn compress_memory(
        &self,
        messages: &[Message],
        cycle_index: Option<u32>,
    ) -> (Vec<Message>, bool) {
        let messages = self.strip_session_memory_context(messages);
        if messages.len() <= 2 {
            return (messages, false);
        }
        let system_message = messages
            .iter()
            .find(|message| message.role == MessageRole::System)
            .cloned();
        let (messages_for_summary, _normalized) = self.normalize_compaction_messages(&messages);
        let (messages_for_summary, artifacts, _compacted_tools) = compact_tool_results(
            &messages_for_summary,
            &ToolResultArtifactConfig {
                workspace: self.config.workspace.clone(),
                artifact_dir: self.config.tool_result_artifact_dir.clone(),
                compact_threshold: self.config.tool_result_compact_threshold,
                keep_last: self.config.tool_result_keep_last,
                excerpt_head: self.config.tool_result_excerpt_head,
                excerpt_tail: self.config.tool_result_excerpt_tail,
            },
            cycle_index,
        );
        let original_request = extract_original_user_request(&messages).unwrap_or_default();
        let summary_prompt = self.build_compress_memory_prompt(&messages_for_summary);
        let mut compressed_memory = self.generate_summary(&summary_prompt, &messages_for_summary);
        if let Ok(summary_data) = serde_json::from_str(&compressed_memory) {
            let restored_context = restore_key_files(
                &summary_data,
                self.config.workspace.as_deref(),
                &PostCompactRestoreConfig {
                    token_model: self.config.model.clone(),
                    ..PostCompactRestoreConfig::default()
                },
            );
            if !restored_context.is_empty() {
                compressed_memory.push_str("\n\n");
                compressed_memory.push_str(&restored_context);
            }
        }
        if let Some(artifact_section) = render_persisted_artifacts_section(&artifacts) {
            compressed_memory.push_str("\n\n");
            compressed_memory.push_str(&artifact_section);
        }

        let mut compacted = Vec::new();
        if let Some(system_message) = system_message {
            compacted.push(system_message);
        }
        compacted.push(Message::user(format!(
            "<Original User Request>\n{original_request}\n</Original User Request>\n\n<Compressed Agent Memory>\n{compressed_memory}\n</Compressed Agent Memory>"
        )));
        (compacted, true)
    }

    fn build_compress_memory_prompt(&self, messages: &[Message]) -> String {
        prompts::build_compress_memory_prompt(
            &self.config.language,
            self.config.summary_event_limit,
            messages,
        )
    }

    fn generate_summary(&self, prompt: &str, messages: &[Message]) -> String {
        if let Some(callback) = &self.config.summary_callback {
            let callback_result = catch_unwind(AssertUnwindSafe(|| {
                callback(
                    prompt,
                    self.config.summary_backend.as_deref(),
                    self.config.summary_model.as_deref(),
                )
            }));
            if let Ok(Some(summary)) = callback_result {
                let normalized = normalize_summary_output(&summary);
                if !normalized.trim().is_empty() {
                    return normalized;
                }
            }
        }
        LocalSummary::from_messages(messages, self.config.summary_event_limit).to_json_string()
    }

    fn normalize_compaction_messages(&self, messages: &[Message]) -> (Vec<Message>, bool) {
        normalization::normalize_compaction_messages(
            messages,
            self.config.tool_calls_keep_last,
            self.config.assistant_no_tool_keep_last,
        )
    }
}