//! Diagnostic prompt source map for context pressure reports. //! //! The report is approximate and describes the runtime sources CodeWhale //! already tracks, without claiming provider-tokenizer parity. Its headline //! (`active_context_estimated_tokens`) is the pressure estimate the context //! meter and the auto-compaction gate read //! (`compaction::estimate_input_tokens_for_pressure`, lifted to the last //! provider-billed prompt). The 1.5x-inflated conservative estimate that //! request-overflow protection uses is reported separately as the overflow //! guard, never as the headline. Per-source entries keep the conservative //! per-text heuristic. mod portable_projection; pub(crate) use portable_projection::source_map as project_source_map; use std::path::Path; use chrono::{SecondsFormat, Utc}; use serde::Serialize; use crate::tui::app::App; use codewhale_config::AppMode; use codewhale_engine::compaction::{ estimate_input_tokens_conservative, estimate_input_tokens_for_pressure, estimate_text_tokens_conservative, }; use codewhale_engine::prompts::CORE_EXECUTION_PROFILE_PROMPT; use codewhale_models::{CacheControl, ContentBlock, Message, SystemPrompt, Tool}; use codewhale_runtime::config::Config; #[cfg(test)] use codewhale_runtime::context_budget::PressureLevel; #[derive(Debug, Clone, Serialize)] pub struct PromptSourceMap { pub entries: Vec, pub total_estimated_tokens: usize, /// Headline: the same pressure estimate the context meter and the /// auto-compaction gate read, so `/context` never disagrees with them. pub active_context_estimated_tokens: usize, /// Secondary: the 1.5x-inflated conservative estimate request-overflow /// protection guards with. `None` when there is no live conversation to /// measure (headless doctor reports). pub overflow_guard_estimated_tokens: Option, pub context_window_tokens: Option, /// Non-secret receipt for the effective context-window value. pub context_window_source: Option, pub budget_used_percent: Option, pub generated_at: String, pub note: String, } /// Inspectable request-prefix context for the current session. /// /// `PromptSourceMap` explains provenance and estimated pressure. This sibling /// type exposes the current assembled system-prompt sections and most recently /// sent model tool catalog so users can audit the prompt plumbing as JSON. #[derive(Debug, Clone, Serialize)] pub struct PromptContext { pub schema_version: u8, pub provider: String, pub model: String, pub system_prompt_state: &'static str, pub tool_catalog_state: &'static str, pub sections: Vec, pub tools: Vec, pub source_map: PromptSourceMap, } #[derive(Debug, Clone, Serialize)] pub struct PromptContextSection { pub index: usize, pub block_type: String, pub cache_control: Option, pub estimated_tokens: usize, pub text: String, } #[derive(Debug, Clone, Serialize)] pub struct SourceEntry { pub source_kind: SourceKind, pub label: String, pub source_path: Option, pub activation_reason: ActivationReason, pub estimated_tokens: usize, pub counting_confidence: CountingConfidence, pub authority_tier: Option, pub truncation_reason: Option, } impl SourceEntry { fn text( source_kind: SourceKind, label: impl Into, source_path: Option, activation_reason: ActivationReason, text: &str, counting_confidence: CountingConfidence, authority_tier: Option, ) -> Self { Self::estimate( source_kind, label, source_path, activation_reason, estimate_text_tokens_conservative(text), counting_confidence, authority_tier, ) } fn estimate( source_kind: SourceKind, label: impl Into, source_path: Option, activation_reason: ActivationReason, estimated_tokens: usize, counting_confidence: CountingConfidence, authority_tier: Option, ) -> Self { Self { source_kind, label: label.into(), source_path, activation_reason, estimated_tokens, counting_confidence, authority_tier, truncation_reason: None, } } fn omitted( source_kind: SourceKind, label: impl Into, source_path: Option, authority_tier: Option, reason: impl Into, ) -> Self { Self { source_kind, label: label.into(), source_path, activation_reason: ActivationReason::Omitted, estimated_tokens: 0, counting_confidence: CountingConfidence::High, authority_tier, truncation_reason: Some(reason.into()), } } fn diagnostic( source_kind: SourceKind, label: impl Into, source_path: Option, activation_reason: ActivationReason, detail: impl Into, estimated_tokens: usize, authority_tier: Option, ) -> Self { Self { source_kind, label: label.into(), source_path, activation_reason, estimated_tokens, counting_confidence: CountingConfidence::High, authority_tier, truncation_reason: Some(detail.into()), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub enum SourceKind { Constitution, UserConstitution, RepoConstitution, ProjectContext, ProjectContextWarning, ProjectContextPack, SkillsBlock, ContextManagement, CompactionRelayTemplate, RuntimePolicy, AuthorityRecap, EnvironmentBlock, UserMemory, SessionGoal, HandoffRelay, ToolSchemas, UserRequest, ConversationHistory, ToolResult, ModelProviderFact, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub enum ActivationReason { AlwaysOn, FilePresent, ConfigEnabled, RuntimeState, PerRequest, Omitted, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub enum CountingConfidence { High, Approximate, } struct ReportBuilder { entries: Vec, } impl ReportBuilder { fn new() -> Self { Self { entries: Vec::new(), } } fn push(&mut self, entry: SourceEntry) { self.entries.push(entry); } /// The window arrives as one resolution rather than a number plus a /// separately chosen label, so the report can never attribute one rung's /// tokens to another rung. fn finish( self, context_window: codewhale_engine::route_runtime::ContextWindowResolution, active_context_estimated_tokens: usize, overflow_guard_estimated_tokens: Option, note: impl Into, ) -> PromptSourceMap { let total_estimated_tokens = self .entries .iter() .map(|entry| entry.estimated_tokens) .sum(); let budget_used_percent = ((active_context_estimated_tokens as f64 / f64::from(context_window.tokens)) * 100.0) .clamp(0.0, 100.0); PromptSourceMap { entries: self.entries, total_estimated_tokens, active_context_estimated_tokens, overflow_guard_estimated_tokens, context_window_tokens: Some(context_window.tokens), context_window_source: Some(context_window.source.label().to_string()), budget_used_percent: Some(budget_used_percent), generated_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true), note: note.into(), } } } pub fn build_context_report(app: &App) -> PromptSourceMap { let context_window = codewhale_engine::route_runtime::ContextWindowResolution { tokens: app.effective_context_window(), source: app.effective_context_window_source(), }; let mut builder = base_source_entries( &app.model, &app.workspace, Some(&app.skills_dir), app.project_context_pack_enabled, &app.skills_config, app.ui_locale.tag(), app.mode, Some(app.plugin_registry.as_ref()), Some(context_window.tokens), app.instruction_assembly.as_deref(), false, ); add_app_runtime_entries(&mut builder, app); builder.finish( context_window, pressure_estimated_tokens(app), Some(estimate_input_tokens_conservative( &app.api_messages, app.system_prompt.as_ref(), )), "Diagnostic source map. The headline is the pressure estimate the context meter and auto-compaction gate use; per-source counts are conservative estimates. All counts may differ from provider billing.", ) } /// The one pressure number the context meter and the auto-compaction gate /// decide on: the un-inflated estimate over the live request, lifted to the /// provider's last billed prompt when that is higher (#5577). fn pressure_estimated_tokens(app: &App) -> usize { let estimated = estimate_input_tokens_for_pressure(&app.api_messages, app.system_prompt.as_ref()); let billed = app .last_billed_input_tokens .and_then(|tokens| usize::try_from(tokens).ok()) .unwrap_or(0); estimated.max(billed) } #[must_use] pub fn build_prompt_context(app: &App) -> PromptContext { let tool_catalog_state = if app.session.last_tool_catalog.is_some() { "last_sent" } else { "not_yet_sent" }; let sections = match app.system_prompt.as_ref() { Some(SystemPrompt::Text(text)) => vec![PromptContextSection { index: 0, block_type: "text".to_string(), cache_control: None, estimated_tokens: estimate_text_tokens_conservative(text), text: text.clone(), }], Some(SystemPrompt::Blocks(blocks)) => blocks .iter() .enumerate() .map(|(index, block)| PromptContextSection { index, block_type: block.block_type.clone(), cache_control: block.cache_control.clone(), estimated_tokens: estimate_text_tokens_conservative(&block.text), text: block.text.clone(), }) .collect(), None => Vec::new(), }; PromptContext { schema_version: 1, provider: app.api_provider.as_str().to_string(), model: app.model.clone(), system_prompt_state: "current_session", tool_catalog_state, sections, tools: app.session.last_tool_catalog.clone().unwrap_or_default(), source_map: build_context_report(app), } } pub fn build_headless_context_report(config: &Config, workspace: &Path) -> PromptSourceMap { let model = config.default_model(); let identity = config.active_provider_identity().ok(); let provider = identity .as_ref() .map_or(codewhale_config::ProviderKind::Custom, |identity| { identity.provider }); let provider_identity = identity .as_ref() .map(|identity| identity.key.to_string()) .unwrap_or_else(|| "unavailable".to_string()); let route = identity.as_ref().and_then(|identity| { codewhale_engine::route_runtime::resolve_runtime_route_for_identity( config, identity, Some(&model), ) .ok() }); // A route we could not resolve does not erase an operator-configured // window: doctor must report the same number the session would use. let context_window = route.as_ref().map_or_else( || { codewhale_engine::route_runtime::resolve_context_window( provider, &model, None, identity .as_ref() .and_then(|identity| config.context_window_for_provider_config(identity)), identity .as_ref() .and_then(|identity| config.model_context_windows_for(identity)), ) }, |route| route.context_window, ); let global_skills_dir = config.skills_dir(); let selected_skills_dir = crate::tui::app::resolve_skills_dir(workspace, &global_skills_dir, config); let settings = codewhale_engine::project_context::InstructionSettings::from_config(config); let configured: Vec<_> = config .instructions_paths() .into_iter() .map(Into::into) .collect(); let mut raw = codewhale_engine::project_context::capture_instruction_context( workspace, &configured, &settings, ); let limits = route.as_ref().and_then(|route| { codewhale_engine::route_budget::known_route_limits(route.candidate.limits()) }); let budgets = codewhale_engine::project_context::InstructionBudgets::resolve_or_default( &settings.budget, context_window.instruction_budget_tokens(), Some( codewhale_engine::route_budget::effective_max_output_tokens_for_turn_with_window( provider, &model, limits, Some(context_window.tokens), None, codewhale_engine::core::engine::turn_loop::resolve_auto_effort( config.reasoning_effort(), provider, &config.active_route_base_url(), &model, ) .as_deref(), ) as usize, ), ); let assembly = codewhale_engine::project_context::assemble_instruction_block(&mut raw, &[], &budgets); let mut builder = base_source_entries( &model, workspace, Some(&selected_skills_dir), config.project_context_pack_enabled(), &config.skills_config(), "en", AppMode::Agent, None, Some(context_window.tokens), Some(&assembly), true, ); let memory_path = config.memory_path(); let memory_enabled = config.memory_enabled(); if let Some(memory_block) = codewhale_runtime::native_memory::native_prompt_block( memory_enabled, &memory_path, workspace, ) { builder.push(SourceEntry::text( SourceKind::UserMemory, "User memory", Some(memory_path.display().to_string()), ActivationReason::ConfigEnabled, &memory_block, CountingConfidence::High, Some(6), )); } else { builder.push(SourceEntry::omitted( SourceKind::UserMemory, "User memory", Some(memory_path.display().to_string()), Some(6), "disabled, missing, or empty", )); } builder.push(SourceEntry::text( SourceKind::ModelProviderFact, format!("Provider facts ({provider_identity})"), None, ActivationReason::RuntimeState, &format!( "provider: {}\nmodel: {}\ncontext_window: {}\ncontext_window_source: {}", provider_identity, model, context_window.tokens, context_window.source.label() ), CountingConfidence::Approximate, None, )); let active_context_estimated_tokens = builder .entries .iter() .map(|entry| entry.estimated_tokens) .sum(); builder.finish( context_window, active_context_estimated_tokens, None, "Headless diagnostic source map. Conversation, tool results, and live TUI state are unavailable in doctor mode.", ) } fn known_tokens(value: Option) -> String { value.map_or_else( || "unknown".to_string(), |tokens| format!("{tokens} tokens"), ) } #[allow(clippy::too_many_arguments)] fn base_source_entries( model: &str, workspace: &Path, skills_dir: Option<&Path>, project_pack_enabled: bool, skills_config: &codewhale_runtime::config::SkillsConfig, locale_tag: &str, mode: AppMode, plugin_registry: Option<&codewhale_plugins::PluginRegistry>, context_window_tokens: Option, instruction_assembly: Option<&codewhale_engine::project_context::InstructionAssembly>, instruction_preview: bool, ) -> ReportBuilder { let mut builder = ReportBuilder::new(); let constitution = codewhale_engine::prompts::compose_default_static_layers(model); builder.push(SourceEntry::text( SourceKind::Constitution, "Bundled constitution, language policy, and output policy", Some( codewhale_engine::prompts::base_prompt_origin() .label() .to_string(), ), ActivationReason::AlwaysOn, &constitution, CountingConfidence::High, Some(1), )); if let Some(block) = codewhale_engine::prompts::load_user_constitution_block() { builder.push(SourceEntry::text( SourceKind::UserConstitution, "User-global constitution", codewhale_config::UserConstitution::path() .ok() .map(|path| path.display().to_string()), ActivationReason::FilePresent, &block, CountingConfidence::High, Some(2), )); } match instruction_assembly { Some(assembly) => { if assembly.constitution_tokens > 0 { let mut entry = SourceEntry::diagnostic( SourceKind::RepoConstitution, "Repository constitution", assembly .constitution_source_path .as_ref() .map(|path| path.display().to_string()), ActivationReason::FilePresent, "included in installed instruction block", assembly.constitution_tokens, Some(4), ); entry.truncation_reason = None; builder.push(entry); } match assembly.block.as_deref() { Some(block) => { let mut entry = SourceEntry::text( SourceKind::ProjectContext, "Project instructions", assembly .source_path .as_ref() .map(|path| path.display().to_string()), ActivationReason::FilePresent, block, CountingConfidence::High, Some(5), ); entry.estimated_tokens = entry .estimated_tokens .saturating_sub(assembly.constitution_tokens); builder.push(entry); } None => builder.push(SourceEntry::omitted( SourceKind::ProjectContext, "Project context and repository instructions", Some(workspace.display().to_string()), Some(5), "no project context block available", )), } let budget_label = if instruction_preview { "Instruction budget (preview)" } else { "Instruction budget" }; let basis = if instruction_preview { "preview from current files and config; no installed receipt and no tool schemas measured" } else { "installed receipt" }; builder.push(SourceEntry::diagnostic( SourceKind::ProjectContext, budget_label, None::, ActivationReason::ConfigEnabled, format!( "{basis}; global {} / {} estimated tokens; project {} / {} estimated tokens; policy {}; usable window {}; static-prefix guard {}", assembly.global_tokens, assembly.budgets.global_tokens, assembly.project_tokens, assembly.budgets.project_tokens, assembly.budgets.policy.as_str(), known_tokens(assembly.budgets.usable_window_tokens), known_tokens(assembly.budgets.static_guard_tokens), ), 0, Some(5), )); let locale = codewhale_localization::resolve_locale(locale_tag); for notice in assembly.notices.iter().filter(|notice| notice.is_loss()) { builder.push(SourceEntry::diagnostic( SourceKind::ProjectContextWarning, format!("Instruction source: {}", notice.label()), notice.path.as_ref().map(|path| path.display().to_string()), ActivationReason::RuntimeState, notice.render(locale), 0, Some(5), )); } if !assembly.warnings.is_empty() { builder.push(SourceEntry::diagnostic( SourceKind::ProjectContextWarning, "Project context warnings", Some(workspace.display().to_string()), ActivationReason::RuntimeState, assembly.warnings.join("\n"), 0, Some(4), )); } } None => builder.push(SourceEntry::omitted( SourceKind::ProjectContext, "Project instructions", None::, Some(5), "installed instruction receipt unavailable", )), } if project_pack_enabled { if let Some(pack) = codewhale_engine::project_context::generate_project_context_pack(workspace) { builder.push(SourceEntry::text( SourceKind::ProjectContextPack, "Project context pack", Some(workspace.display().to_string()), ActivationReason::ConfigEnabled, &pack, CountingConfidence::Approximate, Some(5), )); } } else { builder.push(SourceEntry::omitted( SourceKind::ProjectContextPack, "Project context pack", Some(workspace.display().to_string()), Some(5), "disabled; project_map provides this information on demand", )); } let skills_block = codewhale_skills::render_index( workspace, skills_dir, skills_config, locale_tag, plugin_registry, context_window_tokens, ); if let Some(block) = skills_block { let mut entry = SourceEntry::text( SourceKind::SkillsBlock, "Available skills", skills_dir.map(|path| path.display().to_string()), ActivationReason::FilePresent, &block, CountingConfidence::High, Some(5), ); entry.truncation_reason = codewhale_skills::shortened_skills_note(&block); builder.push(entry); } else { builder.push(SourceEntry::omitted( SourceKind::SkillsBlock, "Available skills", skills_dir.map(|path| path.display().to_string()), Some(5), "no skills discovered", )); } builder.push(SourceEntry::omitted( SourceKind::ContextManagement, format!("{} runtime mode", mode.label()), None, Some(3), "mode enforced by runtime policy and the live tool catalog; no prompt doctrine", )); builder.push(SourceEntry::omitted( SourceKind::CompactionRelayTemplate, "Session relay template", Some("bundled in this codewhale-engine build (COMPACT_TEMPLATE, compiled in)".to_string()), Some(3), "loaded only when /relay is requested; automatic compaction owns its successor brief", )); builder.push(SourceEntry::text( SourceKind::RuntimePolicy, "Core execution discipline", None, ActivationReason::AlwaysOn, CORE_EXECUTION_PROFILE_PROMPT, CountingConfidence::High, Some(3), )); builder.push(SourceEntry::text( SourceKind::AuthorityRecap, "Authority recap", None, ActivationReason::AlwaysOn, codewhale_engine::prompts::effective_authority_recap(), CountingConfidence::High, Some(1), )); builder.push(SourceEntry::text( SourceKind::EnvironmentBlock, "Runtime environment", Some(workspace.display().to_string()), ActivationReason::AlwaysOn, &codewhale_engine::prompts::render_environment_block(workspace, locale_tag), CountingConfidence::High, Some(4), )); add_handoff_entry(&mut builder, workspace); builder } fn add_app_runtime_entries(builder: &mut ReportBuilder, app: &App) { if let Some(memory_block) = codewhale_runtime::native_memory::native_prompt_block( app.use_memory, &app.memory_path, &app.workspace, ) { builder.push(SourceEntry::text( SourceKind::UserMemory, "User memory", Some(app.memory_path.display().to_string()), ActivationReason::ConfigEnabled, &memory_block, CountingConfidence::High, Some(6), )); } else { builder.push(SourceEntry::omitted( SourceKind::UserMemory, "User memory", Some(app.memory_path.display().to_string()), Some(6), "disabled, missing, or empty", )); } if let Some(goal) = app .goal .objective .as_deref() .filter(|goal| !goal.trim().is_empty()) { builder.push(SourceEntry::text( SourceKind::SessionGoal, "Session goal", None, ActivationReason::RuntimeState, goal, CountingConfidence::High, Some(6), )); } else { builder.push(SourceEntry::omitted( SourceKind::SessionGoal, "Session goal", None, Some(6), "no active /goal objective", )); } if let Some(tools) = app.session.last_tool_catalog.as_ref() { let rendered = serde_json::to_string(tools).unwrap_or_default(); builder.push(SourceEntry::text( SourceKind::ToolSchemas, format!("Tool schemas ({} tools)", tools.len()), None, ActivationReason::PerRequest, &rendered, CountingConfidence::Approximate, Some(3), )); } else { builder.push(SourceEntry::omitted( SourceKind::ToolSchemas, "Tool schemas", None, Some(3), "no tool catalog has been sent yet", )); } add_message_entries(builder, &app.api_messages); } fn add_handoff_entry(builder: &mut ReportBuilder, workspace: &Path) { let (path, _) = codewhale_engine::prompts::resolve_handoff_path(workspace); let Some(raw) = std::fs::read_to_string(&path) .ok() .filter(|raw| !raw.trim().is_empty()) else { builder.push(SourceEntry::omitted( SourceKind::HandoffRelay, "Previous session relay", Some( workspace .join(codewhale_engine::prompts::HANDOFF_RELATIVE_PATH) .display() .to_string(), ), Some(6), "no relay artifact found", )); return; }; builder.push(SourceEntry::text( SourceKind::HandoffRelay, "Previous session relay", Some(path.display().to_string()), ActivationReason::FilePresent, &raw, CountingConfidence::High, Some(6), )); } fn add_message_entries(builder: &mut ReportBuilder, messages: &[Message]) { if messages.is_empty() { builder.push(SourceEntry::omitted( SourceKind::ConversationHistory, "Conversation history", None, None, "no API messages yet", )); return; } let latest_user = messages.iter().rposition(|message| message.role == "user"); let mut latest_user_tokens = 0usize; let mut conversation_tokens = 0usize; let mut tool_result_tokens = 0usize; let mut tool_result_count = 0usize; for (index, message) in messages.iter().enumerate() { for block in &message.content { let tokens = estimate_text_tokens_conservative(&content_block_text(block)); match block { ContentBlock::ToolResult { .. } | ContentBlock::ToolSearchToolResult { .. } | ContentBlock::CodeExecutionToolResult { .. } => { tool_result_tokens += tokens; tool_result_count += 1; } ContentBlock::Text { .. } if Some(index) == latest_user => { latest_user_tokens += tokens; } _ => { conversation_tokens += tokens; } } } } if latest_user_tokens > 0 { builder.push(SourceEntry::estimate( SourceKind::UserRequest, "Latest user request", None, ActivationReason::PerRequest, latest_user_tokens, CountingConfidence::High, Some(7), )); } if conversation_tokens > 0 { builder.push(SourceEntry::estimate( SourceKind::ConversationHistory, "Conversation history", None, ActivationReason::RuntimeState, conversation_tokens, CountingConfidence::High, None, )); } if tool_result_count > 0 { builder.push(SourceEntry::estimate( SourceKind::ToolResult, format!("Tool results ({tool_result_count})"), None, ActivationReason::RuntimeState, tool_result_tokens, CountingConfidence::High, None, )); } } fn content_block_text(block: &ContentBlock) -> String { match block { ContentBlock::Text { text, .. } => text.clone(), ContentBlock::Thinking { thinking, .. } => thinking.clone(), ContentBlock::ToolResult { content, .. } => content.clone(), ContentBlock::ToolSearchToolResult { content, .. } | ContentBlock::CodeExecutionToolResult { content, .. } => content.to_string(), ContentBlock::ToolUse { input, .. } | ContentBlock::ServerToolUse { input, .. } => { input.to_string() } ContentBlock::ImageUrl { image_url } => image_url.url.clone(), } } #[cfg(test)] fn pressure_label(percent: Option) -> &'static str { // Delegate to the unified pressure thresholds so this diagnostic label can't // drift from `context_budget::PressureLevel`. `None` (unknown window) keeps // its own sentinel since a level requires a usage percentage. match percent { Some(value) => PressureLevel::from_usage_percent(value).label(), None => "unknown", } } #[cfg(test)] pub fn format_context_report(report: &PromptSourceMap) -> String { codewhale_commands::diagnostics_reports::format_context_report(&project_source_map( report.clone(), )) } #[cfg(test)] pub fn format_context_summary(report: &PromptSourceMap) -> String { codewhale_commands::diagnostics_reports::format_context_summary(&project_source_map( report.clone(), )) } pub fn context_report_json(report: &PromptSourceMap) -> String { codewhale_commands::diagnostics_reports::context_report_json(&project_source_map( report.clone(), )) } #[cfg(test)] mod pressure_fixture_tests; #[cfg(test)] mod tests { use super::*; use codewhale_config::ProviderKind; use codewhale_config::route::RouteLimits; use codewhale_engine::route_runtime::{ContextWindowResolution, ContextWindowSource}; use codewhale_models::Role; use codewhale_models::Tool; use codewhale_runtime::config::Config; use std::fs; use tempfile::tempdir; #[test] fn context_report_json_contains_sources_and_tool_results() { let messages = vec![ Message { role: Role::User, content: vec![ContentBlock::Text { text: "read src/lib.rs".to_string(), cache_control: None, }], }, Message { role: Role::Assistant, content: vec![ContentBlock::ToolResult { execution_id: None, tool_use_id: "call_1".to_string(), content: "large tool output".repeat(40), is_error: None, content_blocks: None, }], }, ]; let mut builder = ReportBuilder::new(); builder.push(SourceEntry::text( SourceKind::Constitution, "Test static", None, ActivationReason::AlwaysOn, "static", CountingConfidence::High, Some(1), )); add_message_entries(&mut builder, &messages); let report = builder.finish( ContextWindowResolution { tokens: 128_000, source: ContextWindowSource::Fallback, }, 123, Some(185), "test", ); let json = context_report_json(&report); assert!(json.contains("\"source_kind\": \"tool_result\"")); assert!(json.contains("\"active_context_estimated_tokens\": 123")); assert!(json.contains("\"overflow_guard_estimated_tokens\": 185")); } #[test] fn context_report_surfaces_repo_constitution_source_and_warnings() { let tmp = tempdir().expect("tempdir"); fs::create_dir(tmp.path().join(".git")).expect("mkdir .git"); fs::create_dir(tmp.path().join(".codewhale")).expect("mkdir .codewhale"); fs::write( tmp.path().join(".codewhale").join("constitution.json"), r#"{ "schema_version": 1, "authority": ["current user request"], "branch_policy": "v0.8.53 work targets the codex/v0.8.53 integration branch, not main" }"#, ) .expect("write constitution"); let report = build_headless_context_report(&Config::default(), tmp.path()); assert!( report.entries.iter().any(|entry| { entry.source_kind == SourceKind::RepoConstitution && entry.source_path.as_deref().is_some_and(|path| { path.replace('\\', "/") .ends_with(".codewhale/constitution.json") }) }), "repo constitution source should be an explicit source-map entry: {:?}", report.entries ); assert!( report.entries.iter().any(|entry| { entry.source_kind == SourceKind::ProjectContextWarning && entry .truncation_reason .as_deref() .is_some_and(|reason| reason.contains("branch_policy appears stale")) && entry.estimated_tokens == 0 }), "repo constitution warnings should be explicit source-map entries: {:?}", report.entries ); let formatted = format_context_report(&report); assert!(formatted.contains("Repository constitution")); assert!(formatted.contains("Project context warnings")); assert!(formatted.contains("/constitution")); assert!(formatted.contains("/setup report")); let json = context_report_json(&report); assert!(json.contains("\"repo_constitution\"")); assert!(json.contains("branch_policy appears stale")); } #[test] fn context_report_shows_actions_and_budgets() { let tmp = tempdir().expect("workspace"); std::fs::write(tmp.path().join("AGENTS.md"), "T".repeat(20_000)).expect("write agents"); let config = { let __cw_config_init_0_0 = Some(codewhale_engine::project_context::InstructionBudgetConfig { project_budget_tokens: Some(50), ..Default::default() }); let mut __cw_config_init_0_value: Config = Default::default(); __cw_config_init_0_value.instruction_budget = __cw_config_init_0_0; __cw_config_init_0_value }; let report = build_headless_context_report(&config, tmp.path()); assert!( report .entries .iter() .any(|entry| entry.label == "Instruction budget (preview)") ); assert!(report.entries.iter().any(|entry| { entry.label.starts_with("Instruction source:") && entry .truncation_reason .as_deref() .is_some_and(|reason| reason.contains("scope_budget")) })); } #[test] fn headless_context_report_uses_kimi_code_k3_route_context() { let tmp = tempdir().expect("workspace"); let config = { let __cw_config_init_1_0 = Some("moonshot".to_string()); let __cw_config_init_1_1 = Some(codewhale_runtime::config::ProvidersConfig { moonshot: codewhale_runtime::config::ProviderConfig { api_key: Some("test-kimi-key".to_string()), base_url: Some( codewhale_config::descriptors::defaults::DEFAULT_KIMI_CODE_BASE_URL .to_string(), ), model: Some( codewhale_config::catalog::reviewed::constants::KIMI_CODE_K3_MODEL .to_string(), ), ..Default::default() }, ..Default::default() }); let mut __cw_config_init_1_value: Config = Default::default(); __cw_config_init_1_value.provider = __cw_config_init_1_0; __cw_config_init_1_value.providers = __cw_config_init_1_1; __cw_config_init_1_value }; let report = build_headless_context_report(&config, tmp.path()); assert_eq!(report.context_window_tokens, Some(262_144)); assert_eq!( report.context_window_source.as_deref(), Some("static Kimi Code safe floor") ); assert!(context_report_json(&report).contains("\"context_window_tokens\": 262144")); } #[test] fn headless_context_report_honors_kimi_code_k3_context_override() { let tmp = tempdir().expect("workspace"); let config = { let __cw_config_init_2_0 = Some("moonshot".to_string()); let __cw_config_init_2_1 = Some(codewhale_runtime::config::ProvidersConfig { moonshot: codewhale_runtime::config::ProviderConfig { api_key: Some("test-kimi-key".to_string()), base_url: Some( codewhale_config::descriptors::defaults::DEFAULT_KIMI_CODE_BASE_URL .to_string(), ), model: Some( codewhale_config::catalog::reviewed::constants::KIMI_CODE_K3_MODEL .to_string(), ), context_window: Some(1_048_576), ..Default::default() }, ..Default::default() }); let mut __cw_config_init_2_value: Config = Default::default(); __cw_config_init_2_value.provider = __cw_config_init_2_0; __cw_config_init_2_value.providers = __cw_config_init_2_1; __cw_config_init_2_value }; let report = build_headless_context_report(&config, tmp.path()); assert_eq!(report.context_window_tokens, Some(1_048_576)); assert_eq!(report.context_window_source.as_deref(), Some("configured")); } fn private_deployment_config(context_window: Option) -> Config { { let __cw_config_init_3_0 = Some("custom".to_string()); let __cw_config_init_3_1 = Some(codewhale_runtime::config::ProvidersConfig { custom: std::collections::HashMap::from([( "custom".to_string(), codewhale_runtime::config::ProviderConfig { kind: Some("openai-compatible".to_string()), api_key: Some("test-private-key".to_string()), base_url: Some("https://private.test/v1".to_string()), model: Some("private-1m-deployment-v9".to_string()), context_window, ..Default::default() }, )]), ..Default::default() }); let mut __cw_config_init_3_value: Config = Default::default(); __cw_config_init_3_value.provider = __cw_config_init_3_0; __cw_config_init_3_value.providers = __cw_config_init_3_1; __cw_config_init_3_value } } /// #5239: a privately deployed id nobody catalogs, with an operator /// override, is a 1M route in the report — no route-resolution outcome may /// silently substitute the legacy window. #[test] fn headless_context_report_honors_a_private_model_context_override() { let tmp = tempdir().expect("workspace"); let report = build_headless_context_report(&private_deployment_config(Some(1_048_576)), tmp.path()); assert_eq!(report.context_window_tokens, Some(1_048_576)); assert_eq!(report.context_window_source.as_deref(), Some("configured")); } /// The same id without an override is a guess, and the report must say so /// against the window it actually used. #[test] fn headless_context_report_marks_an_unknown_private_model_unverified() { let tmp = tempdir().expect("workspace"); let report = build_headless_context_report(&private_deployment_config(None), tmp.path()); assert_eq!(report.context_window_source.as_deref(), Some("fallback")); let formatted = format_context_report(&report); assert!(formatted.contains("this window is a guess"), "{formatted}"); assert!( !formatted.contains("128K"), "the fallback rung must not assert a window it did not read: {formatted}" ); } #[test] fn context_report_marks_whale_md_ignored_without_loading_body() { let tmp = tempdir().expect("tempdir"); fs::write(tmp.path().join("WHALE.md"), "SECRET_LEGACY_WHALE_BODY").expect("write whale"); let report = build_headless_context_report(&Config::default(), tmp.path()); assert!( report.entries.iter().any(|entry| { entry.source_kind == SourceKind::ProjectContextWarning && entry .truncation_reason .as_deref() .is_some_and(|reason| reason.contains("WHALE.md is ignored")) }), "ignored WHALE.md should be visible as a migration warning: {:?}", report.entries ); assert!( !context_report_json(&report).contains("SECRET_LEGACY_WHALE_BODY"), "ignored WHALE.md body must not enter context report" ); } #[test] fn app_context_report_omits_legacy_plain_file_memory() { // The legacy single-file memory path (`~/.deepseek/memory.md` and // friends) was deleted for v0.9.4: only the native // `memory/global/MEMORY.md` store injects. let tmp = tempdir().expect("tempdir"); let memory_path = tmp.path().join("memory.md"); fs::write(&memory_path, "private legacy memory").expect("write memory"); let config: Config = toml::from_str( r#" [memory] enabled = true "#, ) .expect("parse config"); let app = App::new( crate::tui::app::TuiOptions { screen_mode: crate::tui::app::ScreenMode::Inline, use_bracketed_paste: false, memory_path: memory_path.clone(), notes_path: tmp.path().join("notes.txt"), mcp_config_path: tmp.path().join("mcp.json"), use_memory: true, start_in_agent_mode: true, ..crate::tui::test_support::test_tui_options(tmp.path()) }, &config, ); let report = build_context_report(&app); let memory_entry = report .entries .iter() .find(|entry| entry.source_kind == SourceKind::UserMemory) .expect("user memory source entry"); assert_eq!(memory_entry.activation_reason, ActivationReason::Omitted); assert!(!context_report_json(&report).contains("private legacy memory")); } #[test] fn headless_report_counts_project_pack_only_when_configured() { let tmp = tempdir().expect("tempdir"); fs::create_dir(tmp.path().join(".git")).expect("mkdir .git"); fs::create_dir(tmp.path().join("src")).expect("mkdir src"); fs::write(tmp.path().join("src/lib.rs"), "pub fn fixture() {}\n").expect("write fixture"); let default_report = build_headless_context_report(&Config::default(), tmp.path()); let default_pack = default_report .entries .iter() .find(|entry| entry.source_kind == SourceKind::ProjectContextPack) .expect("project pack entry"); assert_eq!(default_pack.activation_reason, ActivationReason::Omitted); assert_eq!(default_pack.estimated_tokens, 0); assert_eq!( default_pack.truncation_reason.as_deref(), Some("disabled; project_map provides this information on demand") ); let relay = default_report .entries .iter() .find(|entry| entry.source_kind == SourceKind::CompactionRelayTemplate) .expect("relay template entry"); assert_eq!(relay.activation_reason, ActivationReason::Omitted); assert_eq!(relay.estimated_tokens, 0); let mut configured = Config::default(); configured.context.project_pack = Some(true); let configured_report = build_headless_context_report(&configured, tmp.path()); let configured_pack = configured_report .entries .iter() .find(|entry| entry.source_kind == SourceKind::ProjectContextPack) .expect("configured project pack entry"); assert_eq!( configured_pack.activation_reason, ActivationReason::ConfigEnabled ); assert!( configured_pack.estimated_tokens > 0, "configured project pack must be counted" ); let environment = configured_report .entries .iter() .find(|entry| entry.source_kind == SourceKind::EnvironmentBlock) .expect("runtime environment entry"); assert_eq!(environment.activation_reason, ActivationReason::AlwaysOn); } #[test] fn app_context_report_counts_configured_project_pack_before_first_turn() { let tmp = tempdir().expect("tempdir"); fs::create_dir(tmp.path().join(".git")).expect("mkdir .git"); fs::create_dir(tmp.path().join("src")).expect("mkdir src"); fs::write(tmp.path().join("src/lib.rs"), "pub fn fixture() {}\n").expect("write fixture"); let mut config = Config::default(); config.context.project_pack = Some(true); let app = App::new( crate::tui::app::TuiOptions { screen_mode: crate::tui::app::ScreenMode::Inline, use_bracketed_paste: false, notes_path: tmp.path().join("notes.txt"), mcp_config_path: tmp.path().join("mcp.json"), start_in_agent_mode: true, ..crate::tui::test_support::test_tui_options(tmp.path()) }, &config, ); assert!( app.system_prompt.is_none(), "fixture must be pre-first-turn" ); let report = build_context_report(&app); let project_pack = report .entries .iter() .find(|entry| entry.source_kind == SourceKind::ProjectContextPack) .expect("project pack entry"); assert_eq!( project_pack.activation_reason, ActivationReason::ConfigEnabled ); assert!(project_pack.estimated_tokens > 0); } #[test] fn headless_context_report_omits_legacy_plain_file_memory() { let tmp = tempdir().expect("tempdir"); let memory_path = tmp.path().join("memory.md"); fs::write(&memory_path, "private legacy memory").expect("write memory"); let mut config: Config = toml::from_str( r#" [memory] enabled = true "#, ) .expect("parse config"); config.memory_path = Some(memory_path.to_string_lossy().into_owned()); let report = build_headless_context_report(&config, tmp.path()); let memory_entry = report .entries .iter() .find(|entry| entry.source_kind == SourceKind::UserMemory) .expect("user memory source entry"); assert_eq!(memory_entry.activation_reason, ActivationReason::Omitted); assert!(!context_report_json(&report).contains("private legacy memory")); } #[test] fn format_summary_lists_largest_sources() { let mut builder = ReportBuilder::new(); builder.push(SourceEntry::estimate( SourceKind::ToolSchemas, "Tool schemas", None, ActivationReason::PerRequest, 500, CountingConfidence::Approximate, Some(3), )); builder.push(SourceEntry::estimate( SourceKind::UserRequest, "Latest user request", None, ActivationReason::PerRequest, 25, CountingConfidence::High, Some(7), )); let report = builder.finish( ContextWindowResolution { tokens: 128_000, source: ContextWindowSource::Fallback, }, 525, Some(800), "test", ); let summary = format_context_summary(&report); assert!(summary.contains("Context Summary")); assert!(summary.contains("Tool schemas (500)")); // The headline is the pressure number; the inflated figure is only // ever the labeled secondary overflow-guard line. assert!(summary.contains("Estimated active context: 525 tokens")); assert!(summary.contains("Overflow guard: 800 tokens")); let full = format_context_report(&report); let headline = full .find("Estimated active context: 525 tokens") .expect("pressure headline"); let guard = full.find("Overflow guard: 800 tokens").expect("guard line"); assert!(headline < guard, "{full}"); } #[test] fn finish_reflects_route_context_window_over_model_default() { // deepseek-v4-pro defaults to a 1M window; a resolved route advertising a // smaller window must win in the report's context_window_tokens. let route_window = 128_000u64; let model_default = codewhale_models::context_window_for_model("deepseek-v4-pro") .expect("model has a default window"); assert_ne!( u64::from(model_default), route_window, "test fixture must differ from the model default to be meaningful" ); let limits = RouteLimits { context_tokens: Some(route_window), input_tokens: None, output_tokens: None, output_declared: false, }; let resolved = codewhale_engine::route_runtime::resolve_context_window( ProviderKind::Deepseek, "deepseek-v4-pro", Some(limits), None, None, ); assert_eq!(resolved.source, ContextWindowSource::Catalog); let builder = ReportBuilder::new(); let report = builder.finish(resolved, 10_000, None, "test"); assert_eq!(report.context_window_tokens, Some(route_window as u32)); assert_eq!(report.context_window_source.as_deref(), Some("catalog")); // Budget percent is computed against the route window, not the default. let expected = (10_000.0 / route_window as f64) * 100.0; let actual = report.budget_used_percent.expect("window known"); assert!( (actual - expected).abs() < 1e-6, "got {actual}, want {expected}" ); } #[test] fn pressure_label_matches_unified_pressure_levels() { // Boundaries mirror context_budget::PressureLevel. assert_eq!(pressure_label(None), "unknown"); assert_eq!(pressure_label(Some(0.0)), "low"); assert_eq!(pressure_label(Some(39.9)), "low"); assert_eq!(pressure_label(Some(40.0)), "moderate"); assert_eq!(pressure_label(Some(74.9)), "moderate"); assert_eq!(pressure_label(Some(75.0)), "high"); assert_eq!(pressure_label(Some(89.9)), "high"); assert_eq!(pressure_label(Some(90.0)), "critical"); assert_eq!(pressure_label(Some(100.0)), "critical"); } #[test] fn tool_schema_entry_serializes_like_runtime_catalog() { let tool = Tool { tool_type: Some("function".to_string()), name: "read_file".to_string(), description: "read a file".to_string(), input_schema: serde_json::json!({"type": "object"}), allowed_callers: None, defer_loading: None, input_examples: None, strict: Some(true), cache_control: None, }; let rendered = serde_json::to_string(&vec![tool]).expect("serialize tool"); assert!(rendered.contains("read_file")); } }