vv-agent 0.11.0

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use super::*;

#[test]
fn runtime_microcompacts_before_full_memory_compaction() {
    let workspace = tempfile::tempdir().expect("workspace");
    let large_tool_payload = "tool output ".repeat(300);
    let llm = MicrocompactInspectingLlmClient::new(large_tool_payload);
    let inspector = llm.clone();
    let mut runtime = AgentRuntime::new(llm);
    runtime.default_workspace = Some(workspace.path().to_path_buf());
    runtime.workspace_backend = Arc::new(vv_agent::workspace::LocalWorkspaceBackend::new(
        workspace.path(),
    ));
    let mut task = AgentTask::new(
        "microcompact_task",
        "demo",
        vv_agent::prompt::PromptBundle::from_instruction_text("system").expect("prompt bundle"),
        "inspect memory",
    );
    task.extra_tool_names.push("bash".to_string());
    task.memory_compact_threshold = 10_000;
    task.metadata
        .insert("model_context_window".to_string(), json!(20_000));
    task.metadata
        .insert("reserved_output_tokens".to_string(), json!(0));
    task.metadata
        .insert("autocompact_buffer_tokens".to_string(), json!(0));
    task.microcompaction_policy = MicrocompactionPolicy::new(0.01, 0.005, 0, 200).expect("policy");
    let (lifecycle, event_handler) = run_event_collector();

    let result = runtime
        .run_with_controls(
            task,
            RuntimeRunControls {
                event_handler: Some(event_handler),
                ..RuntimeRunControls::default()
            },
        )
        .expect("run");

    assert_eq!(result.status, AgentStatus::Completed);
    let second_request = inspector.third_request_messages();
    assert!(
        second_request
            .iter()
            .any(|message| message.content.starts_with(TOOL_RESULT_COMPACT_MARKER)),
        "second request did not include microcompacted tool content: {second_request:#?}"
    );
    assert!(
        second_request
            .iter()
            .all(|message| !message.content.contains("<Compressed Agent Memory>")),
        "microcompact should avoid full summary before threshold: {second_request:#?}"
    );
    let lifecycle = lifecycle.lock().expect("memory lifecycle");
    let started = lifecycle
        .iter()
        .find(|event| {
            matches!(
                event.payload(),
                RunEventPayload::MemoryCompactStarted { .. }
            )
        })
        .expect("microcompact started");
    let completed = lifecycle
        .iter()
        .find(|event| {
            matches!(
                event.payload(),
                RunEventPayload::MemoryCompactCompleted { .. }
            )
        })
        .expect("microcompact completed");
    assert!(matches!(
        started.payload(),
        RunEventPayload::MemoryCompactStarted {
            trigger: MemoryCompactTrigger::MicroThreshold,
            ..
        }
    ));
    assert!(matches!(
        completed.payload(),
        RunEventPayload::MemoryCompactCompleted {
            before_count,
            after_count,
            mode: MemoryCompactMode::Micro,
            changed: true,
            ..
        } if before_count == after_count
    ));
}

#[derive(Clone)]
struct MicrocompactInspectingLlmClient {
    responses_seen: Arc<Mutex<usize>>,
    large_tool_payload: String,
    third_request_messages: Arc<Mutex<Vec<Message>>>,
}

impl MicrocompactInspectingLlmClient {
    fn new(large_tool_payload: String) -> Self {
        Self {
            responses_seen: Arc::new(Mutex::new(0)),
            large_tool_payload,
            third_request_messages: Arc::new(Mutex::new(Vec::new())),
        }
    }

    fn third_request_messages(&self) -> Vec<Message> {
        self.third_request_messages
            .lock()
            .expect("messages poisoned")
            .clone()
    }
}

impl LlmClient for MicrocompactInspectingLlmClient {
    fn complete(&self, request: LlmRequest) -> Result<LLMResponse, LlmError> {
        let mut responses_seen = self
            .responses_seen
            .lock()
            .map_err(|_| LlmError::Request("counter poisoned".to_string()))?;
        *responses_seen += 1;
        if *responses_seen == 1 {
            return Ok(LLMResponse::with_tool_calls(
                "first cycle",
                vec![ToolCall::new(
                    "bash_large",
                    "bash",
                    BTreeMap::from([(
                        "command".to_string(),
                        json!(format!("printf '{}'", self.large_tool_payload)),
                    )]),
                )],
            ));
        }
        if *responses_seen == 2 {
            return Ok(LLMResponse::new(
                "continue once so older tool output can age",
            ));
        }
        *self
            .third_request_messages
            .lock()
            .expect("messages poisoned") = request.messages.clone();
        Ok(LLMResponse::with_tool_calls(
            "finish",
            vec![ToolCall::new(
                "finish_after_microcompact",
                "task_finish",
                BTreeMap::from([("message".to_string(), json!("memory compacted"))]),
            )],
        ))
    }
}