返回 CodeWhale
cache.rs
根目录 / crates / tui / src / commands / groups / debug / cache.rs
1 //! `/cache` command — portable per-turn cache telemetry and inspection.
2 //!
3 //! The host resolves the request route and pricing partitions once. This
4 //! handler checks flags, observes, formats and commits that same inspection;
5 //! no prompt builder or App state is reachable from the command leaf.
6
7 use super::CommandResult;
8 use super::DebugAction as AppAction;
9 use super::cache_format::{
10 format_cache_history, format_cache_stats, format_cache_zones, format_first_divergence,
11 format_static_prefix_status, format_verbose_diff, format_warmup_status,
12 session_cache_rates_line,
13 };
14 use codewhale_command_contract::facets::{
15 CommandDebugDiagnosticsContext, CommandPresentationContext, DebugCacheInspectionObservation,
16 DebugCacheInspectionUnavailable,
17 };
18 use codewhale_command_contract::handler::{CommandCapabilities, CommandContexts, CommandHandler};
19 use codewhale_command_contract::metadata::{
20 CommandInfo as ContractInfo, RegisterCommand as ContractRegisterCommand,
21 };
22
23 pub(in crate::commands) struct CacheCmd;
24
25 const CONTRACT_INFO: ContractInfo = ContractInfo {
26 name: "cache",
27 aliases: &[],
28 usage: "/cache [count|inspect|stats|zones|warmup]",
29 description_key: "cmd_cache_description",
30 };
31
32 impl ContractRegisterCommand<CommandResult> for CacheCmd {
33 fn info() -> &'static ContractInfo {
34 &CONTRACT_INFO
35 }
36
37 fn handler() -> CommandHandler<CommandResult> {
38 CommandHandler::Contextual {
39 capabilities: CommandCapabilities::DEBUG_DIAGNOSTICS
40 .union(CommandCapabilities::PRESENTATION),
41 handler: cache,
42 }
43 }
44 }
45
46 /// Show per-turn prefix-cache telemetry, a static status or an inspection.
47 pub fn cache(contexts: CommandContexts<'_>, arg: Option<&str>) -> CommandResult {
48 let mut parts = contexts.into_parts();
49 let Some(diagnostics) = parts.debug_diagnostics.as_deref_mut() else {
50 return CommandResult::error("Command capability unavailable: debug_diagnostics");
51 };
52 let Some(presentation) = parts.presentation.as_deref_mut() else {
53 return CommandResult::error("Command capability unavailable: presentation");
54 };
55 cache_portable(diagnostics, presentation, arg)
56 }
57
58 fn cache_portable(
59 diagnostics: &mut dyn CommandDebugDiagnosticsContext,
60 presentation: &dyn CommandPresentationContext,
61 arg: Option<&str>,
62 ) -> CommandResult {
63 let arg = arg.map(str::trim).filter(|s| !s.is_empty());
64 let inspect_flags = arg.and_then(|a| {
65 if a == "inspect" {
66 Some("")
67 } else {
68 a.strip_prefix("inspect")
69 .filter(|rest| rest.starts_with(char::is_whitespace))
70 }
71 });
72 if let Some(flags) = inspect_flags {
73 let flags = flags.trim();
74 let verbose = flags.split_whitespace().any(|flag| flag == "--verbose");
75 let json_mode = flags.split_whitespace().any(|flag| flag == "--json");
76 return CommandResult::message(format_cache_inspect(diagnostics, verbose, json_mode));
77 }
78 if matches!(arg, Some("warmup")) {
79 return CommandResult::action(AppAction::CacheWarmup);
80 }
81 if matches!(arg, Some("stats")) {
82 return CommandResult::message(format_cache_stats(&diagnostics.cache_telemetry()));
83 }
84 if matches!(arg, Some("zones")) {
85 return CommandResult::message(format_cache_zones(&diagnostics.cache_telemetry()));
86 }
87
88 let want = match arg {
89 None => 10,
90 Some(raw) => match raw.parse::<usize>() {
91 Ok(n) => n,
92 Err(_) => {
93 return CommandResult::error(format!(
94 "Unknown /cache argument `{raw}`. Usage: /cache [count|inspect [--verbose|--json]|stats|zones|warmup]"
95 ));
96 }
97 },
98 };
99 let telemetry = diagnostics.cache_telemetry();
100 if telemetry.history.is_empty() {
101 let message = (|| {
102 let mut text = presentation.translate("cmd_cache_no_data", &[])?;
103 if let Some(line) = session_cache_rates_line(&telemetry, presentation)? {
104 text.push_str("\n\n");
105 text.push_str(&line);
106 }
107 Ok::<_, String>(text)
108 })();
109 return match message {
110 Ok(text) => CommandResult::message(text),
111 Err(error) => CommandResult::error(error),
112 };
113 }
114 let count = want
115 .min(telemetry.history.len())
116 .min(telemetry.history_capacity);
117 let cost = diagnostics.cost_projection();
118 match format_cache_history(&telemetry, count, &cost, presentation) {
119 Ok(text) => CommandResult::message(text),
120 Err(error) => CommandResult::error(error),
121 }
122 }
123
124 fn format_cache_inspect(
125 diagnostics: &mut dyn CommandDebugDiagnosticsContext,
126 verbose: bool,
127 json_mode: bool,
128 ) -> String {
129 if verbose && json_mode {
130 return "cache inspect: --json and --verbose cannot be combined".to_string();
131 }
132 let observation = match diagnostics.inspect_cache() {
133 Ok(value) => value,
134 Err(DebugCacheInspectionUnavailable::NoConcreteRoute) => {
135 return "cache inspect: Auto has no concrete route yet; send a turn first".to_string();
136 }
137 Err(DebugCacheInspectionUnavailable::MissingCapturedEndpoint) => {
138 return "cache inspect: the restored Auto route has no captured endpoint; send a turn first"
139 .to_string();
140 }
141 };
142 let output = render_inspection(&observation, verbose, json_mode);
143 commit_rendered_inspection(diagnostics, observation, output)
144 }
145
146 pub(super) fn commit_rendered_inspection(
147 diagnostics: &mut dyn CommandDebugDiagnosticsContext,
148 observation: DebugCacheInspectionObservation,
149 output: String,
150 ) -> String {
151 diagnostics.remember_cache_inspection(observation.current);
152 output
153 }
154
155 pub(super) fn json_or_fallback(rendered: Result<String, serde_json::Error>) -> String {
156 rendered.unwrap_or_else(|_| "{\"error\":\"cache inspection serialization failed\"}".to_string())
157 }
158
159 fn render_inspection(
160 observation: &DebugCacheInspectionObservation,
161 verbose: bool,
162 json_mode: bool,
163 ) -> String {
164 let inspection = &observation.current;
165 let previous = observation.previous.as_ref();
166 let warmup_status = format_warmup_status(
167 observation.last_warmup_key.as_ref(),
168 &observation.current_warmup_key,
169 observation.last_warmup_hash_short.as_deref(),
170 &observation.current_warmup_hash_short,
171 );
172 if json_mode {
173 return json_or_fallback(serde_json::to_value(inspection).and_then(|mut value| {
174 if let serde_json::Value::Object(ref mut object) = value {
175 object.insert(
176 "current_warmup_key".to_string(),
177 serde_json::to_value(&observation.current_warmup_key)?,
178 );
179 object.insert(
180 "warmup_status".to_string(),
181 serde_json::Value::String(warmup_status.trim_end().to_string()),
182 );
183 }
184 serde_json::to_string_pretty(&value)
185 }));
186 }
187
188 let mut out = String::new();
189 out.push_str("Cache Inspect\n");
190 out.push_str("Full prompt text is not printed. Hashes are SHA-256 of each rendered layer.\n");
191 out.push_str(&format!(
192 "Base static prefix hash: {}\n",
193 inspection.base_static_prefix_hash
194 ));
195 out.push_str(&format!(
196 "Full request prefix hash: {}\n",
197 inspection.full_request_prefix_hash
198 ));
199 out.push_str(&format!(
200 "Tool catalog hash: {}\n",
201 if inspection.tool_catalog_hash.is_empty() {
202 "(no tools registered)".to_string()
203 } else {
204 inspection.tool_catalog_hash.clone()
205 }
206 ));
207 out.push_str(&format_static_prefix_status(previous, inspection));
208 out.push_str(&format_first_divergence(previous, inspection));
209 out.push_str(&warmup_status);
210 let total_tokens: usize = inspection
211 .layers
212 .iter()
213 .map(|layer| layer.token_estimate)
214 .sum();
215 out.push_str(&format!("Estimated reusable tokens: ~{total_tokens}\n"));
216 out.push('\n');
217
218 for layer in &inspection.layers {
219 let mut line = format!(
220 "{}: {}, chars={}, bytes={}, ~{}tok, hash={}\n",
221 layer.name,
222 layer.stability.label(),
223 layer.char_len,
224 layer.byte_len,
225 layer.token_estimate,
226 layer.sha256
227 );
228 if let Some(tool_result) = &layer.tool_result {
229 let trimmed = line.trim_end_matches('\n').to_string();
230 line = format!(
231 "{trimmed}, original_chars={}, sent_chars={}, truncated={}, deduplicated={}\n",
232 tool_result.original_chars,
233 tool_result.sent_chars,
234 tool_result.truncated,
235 tool_result.deduplicated
236 );
237 }
238 if let Some(turn_meta) = &layer.turn_meta {
239 let trimmed = line.trim_end_matches('\n').to_string();
240 line = format!(
241 "{trimmed}, turn_meta_original_chars={}, turn_meta_sent_chars={}, turn_meta_deduplicated={}, turn_meta_sha256={}\n",
242 turn_meta.original_chars,
243 turn_meta.sent_chars,
244 turn_meta.deduplicated,
245 turn_meta.sha256
246 );
247 }
248 out.push_str(&line);
249 }
250 if verbose {
251 out.push_str("\nVerbose diff\n");
252 if let Some(previous) = previous {
253 out.push_str(&format_verbose_diff(previous, inspection));
254 } else {
255 out.push_str("No previous inspection to compare against.\n");
256 }
257 }
258 out
259 }
260
260 lines RUST