vv-agent 0.11.0

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
mod config;
mod content;
mod info;
mod persist;
mod render;

use std::sync::Arc;

use crate::types::Message;
use crate::workspace::{read_validated_text_artifact, WorkspaceBackend};

pub(crate) use self::content::{
    build_compacted_tool_content, has_recovery_envelope, is_compacted_tool_content,
};
use self::content::{kept_tool_message_indices, should_compact_tool_message};
use self::info::build_tool_call_info;
use self::persist::persist_tool_content;

pub use self::config::ToolResultArtifactConfig;
pub use self::render::{render_persisted_artifacts_section, PersistedArtifact};

pub const TOOL_RESULT_COMPACT_MARKER: &str = "<Tool Result Compact>";

pub fn compact_tool_results(
    messages: &[Message],
    config: &ToolResultArtifactConfig,
    backend: Option<&Arc<dyn WorkspaceBackend>>,
) -> (Vec<Message>, Vec<PersistedArtifact>, bool) {
    if config.compact_threshold == 0 {
        return (messages.to_vec(), Vec::new(), false);
    }
    let tool_info = build_tool_call_info(messages);
    let keep_indices = kept_tool_message_indices(messages, config.keep_last);

    let mut changed = false;
    let mut artifacts = Vec::new();
    let mut compacted = Vec::with_capacity(messages.len());
    for (index, message) in messages.iter().enumerate() {
        let info = message
            .tool_call_id
            .as_deref()
            .and_then(|tool_call_id| tool_info.get(tool_call_id));
        if !should_compact_tool_message(message, index, &keep_indices, config.compact_threshold) {
            if is_compacted_tool_content(&message.content) {
                if let Some(artifact) = persisted_artifact_from_compacted_message(
                    message,
                    info.and_then(|item| item.tool_name.as_deref()),
                    info.and_then(|item| item.arguments.as_deref()),
                ) {
                    artifacts.push(artifact);
                }
            }
            compacted.push(message.clone());
            continue;
        }

        let Some(backend) = backend else {
            compacted.push(message.clone());
            continue;
        };
        let (artifact, excerpt_source) = if let Some(artifact) = message.artifact_ref.as_ref() {
            let Ok(complete_content) = read_validated_text_artifact(backend.as_ref(), artifact)
            else {
                compacted.push(message.clone());
                continue;
            };
            (artifact.clone(), complete_content)
        } else {
            if has_recovery_envelope(&message.content) {
                compacted.push(message.clone());
                continue;
            }
            let Some(artifact) = persist_tool_content(
                &message.content,
                message.tool_call_id.as_deref(),
                backend,
                &config.artifact_namespace,
            ) else {
                compacted.push(message.clone());
                continue;
            };
            (artifact, message.content.clone())
        };
        let artifact_path = artifact.path.clone();
        if artifact_path.is_empty() {
            compacted.push(message.clone());
            continue;
        }
        let content = build_compacted_tool_content(
            &excerpt_source,
            &artifact_path,
            info.and_then(|item| item.tool_name.as_deref())
                .unwrap_or("unknown"),
            config,
        );
        let mut updated = message.clone();
        updated.content = content;
        updated.artifact_ref = Some(artifact);
        compacted.push(updated);
        artifacts.push(PersistedArtifact {
            path: artifact_path,
            tool_name: info.and_then(|item| item.tool_name.clone()),
            arguments: info.and_then(|item| item.arguments.clone()),
        });
        changed = true;
    }
    (compacted, artifacts, changed)
}

fn persisted_artifact_from_compacted_message(
    message: &Message,
    fallback_tool_name: Option<&str>,
    arguments: Option<&str>,
) -> Option<PersistedArtifact> {
    let mut tool_name = fallback_tool_name.map(str::to_string);
    for line in message.content.lines() {
        let line = line.trim();
        if tool_name.is_none() {
            tool_name = line
                .strip_prefix("tool_name:")
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(str::to_string);
        }
    }
    Some(PersistedArtifact {
        path: message.artifact_ref.as_ref()?.path.clone(),
        tool_name,
        arguments: arguments.map(str::to_string),
    })
}