vtcode-core 0.175.6

Core library for VT Code - a Rust-based terminal coding agent
Documentation
use super::*;
use crate::core::agent::state::validate_history_invariants;
use crate::llm::provider::{Message, MessageRole};

#[tokio::test]
async fn headless_noop_compaction_preserves_shared_history() {
    struct NoOpCase {
        enabled: bool,
        capacity: usize,
        projected_tokens: usize,
        suppressed: u8,
    }
    let cases = [
        NoOpCase {
            enabled: true,
            capacity: 32_000,
            projected_tokens: 1_000,
            suppressed: crate::compaction::SUPPRESS_NONE,
        },
        NoOpCase {
            enabled: false,
            capacity: 32_000,
            projected_tokens: 30_000,
            suppressed: crate::compaction::SUPPRESS_NONE,
        },
        NoOpCase {
            enabled: true,
            capacity: 32_000,
            projected_tokens: 30_000,
            suppressed: crate::compaction::SUPPRESS_STICKY,
        },
        NoOpCase {
            enabled: true,
            capacity: 0,
            projected_tokens: 1_000,
            suppressed: crate::compaction::SUPPRESS_NONE,
        },
    ];
    for case in cases {
        let temp = TempDir::new().unwrap();
        let mut cfg = VTCodeConfig::default();
        cfg.agent.harness.tool_result_clearing.enabled = false;
        cfg.agent.harness.auto_compaction_enabled = case.enabled;
        cfg.context.max_context_tokens = case.capacity;
        let mut runner = Box::pin(make_runner(&temp, cfg, "noop-sharing")).await;
        let provider = ClearingProvider {
            name: "queued-test-provider",
            capacity: case.capacity,
            requests: Arc::new(Mutex::new(Vec::new())),
        };
        runner.provider_client = Box::new(provider.clone());
        let mut bundle = runner.build_validated_runtime_prompt_bundle(false).await.unwrap();
        let mut state = AgentSessionState::new("noop-sharing".into(), 1, 1, case.capacity);
        state.messages = Arc::new(vec![Message::user("retained source ".repeat(1_000))]);
        state.auto_compact_suppressed = case.suppressed;
        let projection = runner.project_request_history(&state, "unknown-clearing-model", 0, 2_000);
        assert!(Arc::ptr_eq(&projection, &state.messages));
        let mut recorder = ExecEventRecorder::new("noop-sharing", None, None);
        let compacted = runner
            .maybe_auto_compact(
                &mut state,
                &mut recorder,
                "unknown-clearing-model",
                2,
                case.projected_tokens,
                2_000,
                &mut bundle.request_envelope,
            )
            .await;
        assert!(!compacted);
        assert!(Arc::ptr_eq(&projection, &state.messages), "no-op compaction must not copy shared history");
        assert_eq!(state.auto_compact_suppressed, case.suppressed);
        assert!(provider.requests.lock().is_empty());
        let events = recorder.into_events();
        assert_eq!(events.len(), 1);
        assert!(matches!(events[0], ThreadEvent::ThreadStarted(_)));
    }
}

#[derive(Clone)]
struct ClearingProvider {
    name: &'static str,
    capacity: usize,
    requests: Arc<Mutex<Vec<LLMRequest>>>,
}

fn is_summary_request(request: &LLMRequest) -> bool {
    request.messages.iter().any(|message| {
        message
            .content
            .as_text()
            .contains("Summarize the conversation so far using this exact structure")
    })
}

#[async_trait]
impl LLMProvider for ClearingProvider {
    fn name(&self) -> &str {
        self.name
    }

    fn effective_context_size(&self, _model: &str) -> usize {
        self.capacity
    }

    fn supports_context_edits(&self, _model: &str) -> bool {
        self.name == "anthropic"
    }

    async fn generate(&self, request: LLMRequest) -> Result<LLMResponse, LLMError> {
        let summary = is_summary_request(&request);
        self.requests.lock().push(request);
        Ok(text_response(if summary {
            "## Goal\nInspect evidence.\n## Progress\nOld evidence inspected.\n## Next Steps\nFinish the inspection."
        } else {
            "Inspection complete."
        }))
    }

    fn supported_models(&self) -> Vec<String> {
        vec![ModelId::default().as_str().into_owned()]
    }

    fn validate_request(&self, _request: &LLMRequest) -> Result<(), LLMError> {
        Ok(())
    }
}

fn evidence_body(label: &str, tokens: usize) -> String {
    let line = format!("{label}: measured evidence with a distinct observation.\n");
    let mut lower = 0;
    let mut upper = tokens.max(1);
    while lower < upper {
        let repetitions = lower + (upper - lower) / 2;
        let body = line.repeat(repetitions);
        if Message::tool_response("probe".into(), body).estimate_tokens() < tokens {
            lower = repetitions + 1;
        } else {
            upper = repetitions;
        }
    }
    line.repeat(lower)
}

fn evidence_history(capacity: usize, protected_large: bool) -> Vec<Message> {
    let mut messages = vec![Message::user("Inspect recorded evidence".into())];
    for (batch, count) in [1, 4, 3].into_iter().enumerate() {
        if batch > 0 {
            messages.push(Message::user(format!("Continue inspection for batch {batch}")));
        }
        let mut assistant = Message::assistant(format!("Inspect batch {batch}"));
        assistant.tool_calls = Some(
            (0..count)
                .map(|call| {
                    ToolCall::function(
                        format!("evidence_{batch}_{call}"),
                        "read_file".into(),
                        format!("{{\"path\":\"batch_{batch}_{call}.txt\"}}"),
                    )
                })
                .collect(),
        );
        messages.push(assistant);
        for call in (0..count).rev() {
            let tokens = match batch {
                0 => capacity / 2,
                1 => capacity / 10,
                _ if protected_large => capacity * 4 / 15,
                _ => 200 + call * 100,
            };
            messages.push(Message::tool_response(
                format!("evidence_{batch}_{call}"),
                evidence_body(&format!("raw_evidence_{batch}_{call}"), tokens),
            ));
        }
    }
    messages
}

#[tokio::test]
async fn headless_clearing_stages_before_compaction_capacity_sweep() {
    for capacity in [32_000, 64_000, 96_000, 128_000] {
        for (recent_batches, protected_large) in [(0, false), (0, true), (2, false), (2, true)] {
            let temp = TempDir::new().unwrap();
            let mut cfg = VTCodeConfig::default();
            cfg.agent.harness.orchestration_mode = vtcode_config::core::agent::HarnessOrchestrationMode::Single;
            cfg.agent.harness.auto_compaction_enabled = true;
            cfg.agent.harness.tool_result_clearing.keep_recent_tool_batches = recent_batches;
            cfg.context.max_context_tokens = capacity;
            cfg.context.preserve_recent_turns = 2;
            cfg.automation.full_auto.max_turns = 1;
            let mut runner = Box::pin(make_runner(&temp, cfg, "clearing-sweep")).await;
            let provider = ClearingProvider {
                name: "queued-test-provider",
                capacity,
                requests: Arc::new(Mutex::new(Vec::new())),
            };
            runner.provider_client = Box::new(provider.clone());
            let full_history = evidence_history(capacity, protected_large);
            runner.bootstrap_messages = full_history.clone();

            let bundle = runner.build_validated_runtime_prompt_bundle(false).await.unwrap();
            let overhead = bundle.system_prompt_report.token_estimate as usize
                + crate::llm::usage_cost::estimate_tool_definition_tokens(&bundle.request_envelope.ordered_tools())
                    as usize;
            let mut state = AgentSessionState::new("projection".into(), 1, 1, capacity);
            state.messages = Arc::new(full_history.clone());
            let projected = runner.project_request_history(&state, &ModelId::default().as_str(), overhead, 2_000);
            let projected_tokens = projected.iter().map(Message::estimate_tokens).sum::<usize>();
            let raw_tokens = full_history.iter().map(Message::estimate_tokens).sum::<usize>();
            let threshold = (capacity - 2_000) * 75 / 100;
            assert!(raw_tokens + overhead >= threshold, "fixture must pressure full history");
            assert_eq!(projected_tokens + overhead >= threshold, protected_large);
            assert_eq!(state.messages.as_ref(), &full_history, "projection must not alter durable history");
            assert!(validate_history_invariants(&projected).is_valid());
            let retained: Vec<_> = projected
                .iter()
                .filter(|message| {
                    message.role == MessageRole::Tool
                        && !message.content.as_text().contains("\"cleared\":\"tool_result\"")
                })
                .map(|message| message.tool_call_id.as_deref().unwrap())
                .collect();
            let expected = if recent_batches == 0 {
                vec!["evidence_2_2", "evidence_2_1", "evidence_2_0"]
            } else {
                vec![
                    "evidence_1_3",
                    "evidence_1_2",
                    "evidence_1_1",
                    "evidence_1_0",
                    "evidence_2_2",
                    "evidence_2_1",
                    "evidence_2_0",
                ]
            };
            assert_eq!(retained, expected);

            let mut inspection = task("Inspect retained evidence", "clearing-sweep");
            inspection.description = "Inspect the retained file evidence and reconcile the observations across the three historical tool batches. ".repeat(4);
            assert!(!AgentRunner::is_simple_task(&inspection, &[]));
            let result = Box::pin(runner.execute_task(&inspection, &[])).await.unwrap();
            let requests = provider.requests.lock();
            let summaries = requests.iter().filter(|request| is_summary_request(request)).count();
            assert_eq!(summaries, usize::from(protected_large), "capacity={capacity}");
            assert_eq!(requests.len(), 1 + summaries);
            let final_request = requests.last().unwrap();
            assert_eq!(final_request.max_tokens, Some(2_000));
            assert!(validate_history_invariants(&final_request.messages).is_valid());
            assert!(!result.warnings.iter().any(|warning| warning.starts_with("Pre-flight check:")));
            if !protected_large {
                let tool_results: Vec<_> = final_request
                    .messages
                    .iter()
                    .filter(|message| message.role == MessageRole::Tool)
                    .collect();
                assert_eq!(tool_results.len(), 8);
                assert_eq!(
                    tool_results
                        .iter()
                        .filter(|message| message.content.as_text().contains("\"cleared\":\"tool_result\""))
                        .count(),
                    if recent_batches == 0 { 5 } else { 1 }
                );
                assert!(
                    final_request
                        .messages
                        .iter()
                        .any(|message| message.content.as_text().contains("raw_evidence_2_0"))
                );
                assert!(
                    !result
                        .thread_events
                        .iter()
                        .any(|event| matches!(event, ThreadEvent::ThreadCompactBoundary(_)))
                );
            } else {
                let boundary = result
                    .thread_events
                    .iter()
                    .find_map(|event| match event {
                        ThreadEvent::ThreadCompactBoundary(boundary) => Some(boundary),
                        _ => None,
                    })
                    .expect("compaction boundary");
                let artifact = boundary.history_artifact_path.as_ref().expect("full-history artifact");
                let original = fs::read_to_string(workspace_root(&temp).join(artifact)).unwrap();
                assert!(original.contains("raw_evidence_0_0"), "compaction receives full original evidence");
                assert!(!original.contains("\"cleared\":\"tool_result\""));
            }
            eprintln!(
                "clearing fixture: capacity={capacity} protected_large={protected_large} raw_tokens={raw_tokens} projected_tokens={projected_tokens} overhead={overhead} retained_results={} summary_calls={summaries}",
                retained.len()
            );
        }
    }
}

#[tokio::test]
async fn headless_clearing_preserves_opt_out_and_anthropic_local_fallback() {
    for (name, enabled) in [("queued-test-provider", false), ("anthropic", true)] {
        let temp = TempDir::new().unwrap();
        let mut cfg = VTCodeConfig::default();
        cfg.agent.harness.orchestration_mode = vtcode_config::core::agent::HarnessOrchestrationMode::Single;
        cfg.agent.harness.tool_result_clearing.enabled = enabled;
        cfg.context.max_context_tokens = 32_000;
        cfg.context.preserve_recent_turns = 2;
        cfg.automation.full_auto.max_turns = 1;
        let mut runner = Box::pin(make_runner(&temp, cfg, "clearing-gate")).await;
        let provider = ClearingProvider {
            name,
            capacity: 32_000,
            requests: Arc::new(Mutex::new(Vec::new())),
        };
        runner.provider_client = Box::new(provider.clone());
        runner.bootstrap_messages = evidence_history(32_000, false);
        Box::pin(runner.execute_task(&task("Inspect retained evidence", "clearing-gate"), &[]))
            .await
            .unwrap();
        let requests = provider.requests.lock();
        assert_eq!(requests.iter().filter(|request| is_summary_request(request)).count(), usize::from(!enabled));
        assert!(requests.last().unwrap().context_management.is_none());
        assert!(validate_history_invariants(&requests.last().unwrap().messages).is_valid());
    }
}