vv-agent 0.8.0

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

#[test]
fn memory_capacity_defaults_match_contract_without_rewriting_explicit_values() {
    let contract = contract();
    let expected = &contract["capacity_contract"];

    assert_eq!(
        AgentTask::new("task", "model", "system", "user").memory_compact_threshold,
        expected["configured_default_threshold"].as_u64().unwrap()
    );
    assert_eq!(
        MemoryManagerConfig::default().compact_threshold,
        expected["configured_default_threshold"].as_u64().unwrap()
    );

    let mut explicit = AgentTask::new("task", "model", "system", "user");
    explicit.memory_compact_threshold = 128_000;
    let restored: AgentTask =
        serde_json::from_value(serde_json::to_value(&explicit).expect("serialize task"))
            .expect("restore task");
    assert_eq!(restored.memory_compact_threshold, 128_000);
}

#[test]
fn runtime_context_window_resolution_matches_contract_cases_and_zero_capability() {
    let contract = contract();
    let mut cases = contract["capacity_contract"]["context_window_resolution"]["cases"]
        .as_array()
        .expect("context resolution cases")
        .clone();
    cases.push(json!({
        "name": "zero_resolved_capability_uses_derived_planning_context",
        "input": {
            "task_metadata_model_context_window": 0,
            "resolved_model_context_window": 0
        },
        "expected_model_context_window": contract["capacity_contract"]["unknown_context_window_strategy"]["default_model_context_window"]
    }));

    for case in &cases {
        let input = &case["input"];
        let workspace = tempfile::tempdir().expect("capacity workspace");
        let mut runtime = AgentRuntime::new(PromptTooLongThenSuccess::new(1));
        if let Some(context_length) = input["resolved_model_context_window"].as_u64() {
            let settings_file = workspace.path().join("llm_settings.json");
            std::fs::write(
                &settings_file,
                json!({
                    "VERSION": "2",
                    "endpoints": [{
                        "id": "deepseek-primary",
                        "api_key": "sk-test",
                        "api_base": "https://api.deepseek.test"
                    }],
                    "backends": {
                        "deepseek": {
                            "models": {
                                "deepseek-v4-pro": {
                                    "id": "deepseek-v4-pro",
                                    "endpoints": ["deepseek-primary"],
                                    "context_length": context_length,
                                    "max_output_tokens": 32_000
                                }
                            }
                        }
                    }
                })
                .to_string(),
            )
            .expect("capacity settings");
            runtime = runtime
                .with_settings_file(settings_file)
                .with_default_backend("deepseek");
        }

        let mut task = AgentTask::new(
            format!("context-resolution-{}", case["name"].as_str().unwrap()),
            "deepseek-v4-pro",
            "system",
            "continue",
        );
        task.max_cycles = 1;
        task.no_tool_policy = NoToolPolicy::Finish;
        task.metadata.insert(
            "model_context_window".to_string(),
            input["task_metadata_model_context_window"].clone(),
        );
        let logs = Arc::new(Mutex::new(Vec::<RunEvent>::new()));
        let log_sink = logs.clone();

        runtime
            .run_with_controls(
                task,
                RuntimeRunControls {
                    event_handler: Some(Arc::new(move |event| {
                        if matches!(
                            event.payload(),
                            RunEventPayload::MemoryCompactStarted { .. }
                        ) {
                            log_sink
                                .lock()
                                .expect("context resolution logs")
                                .push(event.clone());
                        }
                    })),
                    ..RuntimeRunControls::default()
                },
            )
            .unwrap_or_else(|error| panic!("{}: {error}", case["name"]));

        let logs = logs.lock().expect("context resolution logs");
        let started = logs
            .iter()
            .find(|event| {
                matches!(
                    event.payload(),
                    RunEventPayload::MemoryCompactStarted {
                        trigger: MemoryCompactTrigger::PromptTooLong,
                        ..
                    }
                )
            })
            .unwrap_or_else(|| panic!("{}: prompt-too-long event", case["name"]));
        let RunEventPayload::MemoryCompactStarted {
            model_context_window,
            ..
        } = started.payload()
        else {
            unreachable!("matched memory compact started")
        };
        assert_eq!(
            Value::from(*model_context_window),
            case["expected_model_context_window"],
            "{}",
            case["name"]
        );
    }
}

#[test]
fn runtime_capacity_resolution_matches_every_contract_case() {
    let contract = contract();
    let capacity = &contract["capacity_contract"];

    for case in capacity["cases"].as_array().expect("capacity cases") {
        let input = &case["input"];
        let expected = &case["expected"];
        let mut task = AgentTask::new(
            format!("capacity-{}", case["name"].as_str().unwrap()),
            "capacity-model",
            "system",
            "continue",
        );
        task.initial_messages = vec![
            Message::system("system"),
            Message::user("first"),
            Message::assistant("working"),
        ];
        task.max_cycles = 1;
        task.no_tool_policy = NoToolPolicy::Finish;
        task.memory_compact_threshold = input["configured_threshold"].as_u64().unwrap();
        task.metadata.insert(
            "model_context_window".to_string(),
            input["model_context_window"].clone(),
        );
        task.metadata.insert(
            "autocompact_buffer_tokens".to_string(),
            input["autocompact_buffer_tokens"].clone(),
        );
        if let Some(value) = input["task_metadata_reserved_output_tokens"].as_u64() {
            task.metadata
                .insert("reserved_output_tokens".to_string(), Value::from(value));
        }
        if let Some(value) = input["model_max_output_tokens"].as_u64() {
            task.metadata
                .insert("model_max_output_tokens".to_string(), Value::from(value));
        }
        if let Some(value) = input["effective_model_max_tokens"].as_u64() {
            task.model_settings = Some(
                ModelSettings::builder()
                    .max_tokens(u32::try_from(value).expect("request limit fits u32"))
                    .build(),
            );
        }

        let logs = Arc::new(Mutex::new(Vec::<RunEvent>::new()));
        let log_sink = logs.clone();
        AgentRuntime::new(PromptTooLongThenSuccess::new(1))
            .run_with_controls(
                task,
                RuntimeRunControls {
                    event_handler: Some(Arc::new(move |event| {
                        if matches!(
                            event.payload(),
                            RunEventPayload::MemoryCompactStarted { .. }
                                | RunEventPayload::MemoryCompactCompleted { .. }
                        ) {
                            log_sink.lock().expect("capacity logs").push(event.clone());
                        }
                    })),
                    ..RuntimeRunControls::default()
                },
            )
            .unwrap_or_else(|error| panic!("{}: {error}", case["name"]));

        let logs = logs.lock().expect("capacity logs");
        let started = logs
            .iter()
            .find(|event| {
                matches!(
                    event.payload(),
                    RunEventPayload::MemoryCompactStarted {
                        trigger: MemoryCompactTrigger::PromptTooLong,
                        ..
                    }
                )
            })
            .unwrap_or_else(|| panic!("{}: forced started event", case["name"]));
        let started = serde_json::to_value(started).expect("capacity started event wire");
        assert_eq!(
            started["configured_threshold"],
            input["configured_threshold"]
        );
        assert_eq!(
            started["effective_threshold"],
            expected["effective_threshold"]
        );
        assert_eq!(
            started["microcompact_threshold"],
            expected["microcompact_threshold"]
        );
        assert_eq!(
            started["model_context_window"],
            input["model_context_window"]
        );
        assert_eq!(
            started["model_max_output_tokens"],
            input["model_max_output_tokens"]
        );
        assert_eq!(
            started["reserved_output_tokens"],
            expected["reserved_output_tokens"]
        );
        assert_eq!(
            started["reserved_output_source"],
            expected["reserved_output_source"]
        );
        assert_eq!(
            started["autocompact_buffer_tokens"],
            input["autocompact_buffer_tokens"]
        );
    }
}

fn simultaneous_warning_microcompact_messages(recent_tool_chars: usize) -> Vec<Message> {
    vec![
        Message::system("system"),
        Message::user("start"),
        Message {
            tool_calls: vec![ToolCall::new("call_old", "read_file", BTreeMap::new())],
            ..Message::assistant("old tool call")
        },
        Message::tool("x".repeat(800), "call_old"),
        Message {
            tool_calls: vec![ToolCall::new("call_recent", "read_file", BTreeMap::new())],
            ..Message::assistant("recent tool call")
        },
        Message::tool("y".repeat(recent_tool_chars), "call_recent"),
    ]
}

#[test]
fn warning_is_evaluated_from_post_microcompact_usage_on_both_threshold_paths() {
    let contract = contract();
    assert_eq!(
        contract["compaction_events"]["simultaneous_warning_and_microcompact"]["order"],
        json!([
            "microcompact_eligible_old_tool_results",
            "recalculate_effective_length",
            "append_memory_warning_only_if_post_microcompact_length_remains_eligible"
        ])
    );

    for initial_usage in [3_800, 4_200] {
        let mut manager = MemoryManager::new(MemoryManagerConfig {
            compact_threshold: 4_000,
            model_context_window: 4_000,
            reserved_output_tokens: 0,
            autocompact_buffer_tokens: 0,
            warning_threshold_percentage: 90,
            include_memory_warning: true,
            language: "en-US".to_string(),
            microcompact_keep_recent_cycles: 1,
            microcompact_min_result_length: 500,
            ..MemoryManagerConfig::default()
        });

        let (compacted, changed) = manager.compact_for_cycle_with_usage(
            &simultaneous_warning_microcompact_messages(800),
            4,
            false,
            Some(initial_usage),
            None,
        );

        assert!(changed, "initial usage {initial_usage}");
        assert!(
            compacted
                .iter()
                .any(|message| message.content == CLEARED_MARKER),
            "initial usage {initial_usage}"
        );
        assert!(
            compacted.iter().all(|message| !message
                .content
                .contains("current memory usage has exceeded")),
            "warning used the stale pre-microcompact length for {initial_usage}"
        );
        assert!(
            compacted
                .iter()
                .all(|message| !message.content.contains("<Compressed Agent Memory>")),
            "microcompact should avoid summary for {initial_usage}"
        );
    }
}

#[test]
fn warning_is_retained_when_post_microcompact_usage_remains_eligible() {
    let messages = simultaneous_warning_microcompact_messages(8_000);
    let mut manager = MemoryManager::new(MemoryManagerConfig {
        reserved_output_tokens: 0,
        autocompact_buffer_tokens: 0,
        warning_threshold_percentage: 90,
        include_memory_warning: true,
        language: "en-US".to_string(),
        microcompact_keep_recent_cycles: 1,
        microcompact_min_result_length: 500,
        ..MemoryManagerConfig::default()
    });
    let (post_microcompact, cleared) = manager.microcompact_messages(&messages, 4);
    assert_eq!(cleared, 1);
    let post_tokens = count_messages_tokens(&post_microcompact, &manager.config.model);
    assert!(post_tokens > 900);
    let full_threshold = post_tokens + 100;
    manager.config.compact_threshold = full_threshold;
    manager.config.model_context_window = full_threshold;

    let (compacted, changed) =
        manager.compact_for_cycle_with_usage(&messages, 4, false, Some(full_threshold), None);

    assert!(changed);
    assert!(compacted
        .iter()
        .any(|message| message.content == CLEARED_MARKER));
    assert!(compacted.iter().any(|message| message
        .content
        .contains("current memory usage has exceeded")));
}