| 1 | //! Single portable renderer for the prepared-tool request projection. |
| 2 | //! |
| 3 | //! The authoritative request capture and bounding policy remain TUI-owned. |
| 4 | //! This module formats the already-bounded, contract-owned semantic snapshot; |
| 5 | //! no registry, App, engine or client is reachable from it. |
| 6 | |
| 7 | use codewhale_command_contract::facets::{ |
| 8 | DebugBoundedString as BoundedString, DebugEvidence as Evidence, |
| 9 | DebugProviderAvailability as ProviderAvailability, DebugToolProvenance, DebugToolSnapshot, |
| 10 | DebugToolVisibility, |
| 11 | }; |
| 12 | |
| 13 | #[must_use] |
| 14 | pub fn render_tool_snapshot_text(snapshot: &DebugToolSnapshot) -> String { |
| 15 | let mut out = String::new(); |
| 16 | out.push_str("Prepared Model-Client Tool Request (read-only)\n"); |
| 17 | out.push_str(&format!("Capture source: {}\n", snapshot.capture_source)); |
| 18 | out.push_str(&format!("Delivery: {}\n", snapshot.delivery_status)); |
| 19 | out.push_str(&format!( |
| 20 | "Turn: {}\nTurn truncated: {}\n", |
| 21 | json_string(&snapshot.turn_id.value), |
| 22 | yes_no(snapshot.turn_id.truncated) |
| 23 | )); |
| 24 | out.push_str(&format!("Step: {}\n", snapshot.step)); |
| 25 | if let Some(terminal) = &snapshot.terminal { |
| 26 | out.push_str("Terminal diagnostics (observed facts; effective_max_steps null means uncapped, other nulls mean unknown):\n"); |
| 27 | if let Ok(json) = serde_json::to_string_pretty(terminal) { |
| 28 | out.push_str(&json); |
| 29 | out.push('\n'); |
| 30 | } |
| 31 | } |
| 32 | out.push_str(&format!( |
| 33 | "Tools field: {}\nTool count: {}\n", |
| 34 | if snapshot.tools_field_present { |
| 35 | "present" |
| 36 | } else { |
| 37 | "absent" |
| 38 | }, |
| 39 | snapshot.tool_count |
| 40 | )); |
| 41 | out.push_str(&format!( |
| 42 | "Rendered tools: {}; omitted by render bound: {}\n", |
| 43 | snapshot.rendered_tool_count, snapshot.omitted_tool_count |
| 44 | )); |
| 45 | out.push_str(&format!( |
| 46 | "Model-client payload measurement: {}\n", |
| 47 | snapshot.payload_measurement_status |
| 48 | )); |
| 49 | out.push_str(&format_optional_usize( |
| 50 | "Model-client tool JSON bytes", |
| 51 | snapshot.payload_json_bytes, |
| 52 | )); |
| 53 | out.push_str(&format_optional_string( |
| 54 | "Active tool catalog digest (same digest as the request manifest)", |
| 55 | snapshot.active_tool_catalog_sha256.as_deref(), |
| 56 | )); |
| 57 | out.push_str( |
| 58 | "Provider-wire tool payload: unavailable (the provider adapter may transform or omit model-client fields)\n", |
| 59 | ); |
| 60 | match &snapshot.provider { |
| 61 | ProviderAvailability::Available { provider, model } => out.push_str(&format!( |
| 62 | "Provider: {} (resolved model client)\nModel: {}\n", |
| 63 | json_string(provider), |
| 64 | json_string(model) |
| 65 | )), |
| 66 | ProviderAvailability::Unavailable { reason } => { |
| 67 | out.push_str(&format!("Provider: unavailable ({reason})\n")); |
| 68 | } |
| 69 | ProviderAvailability::Unknown => { |
| 70 | out.push_str("Provider: unknown (no model-client receipt captured)\n"); |
| 71 | } |
| 72 | } |
| 73 | out.push_str(&format!( |
| 74 | "Registry facts: {}\n", |
| 75 | if snapshot.registry_facts_present { |
| 76 | "captured" |
| 77 | } else { |
| 78 | "not captured" |
| 79 | } |
| 80 | )); |
| 81 | match &snapshot.registry_tool_count { |
| 82 | Evidence::Known { value } => { |
| 83 | out.push_str(&format!("Registered tools: {value}\n")); |
| 84 | } |
| 85 | Evidence::Unknown { reason } => { |
| 86 | out.push_str(&format!("Registered tools: unknown ({reason})\n")); |
| 87 | } |
| 88 | } |
| 89 | match &snapshot.registry_only_tools { |
| 90 | Evidence::Known { value } => { |
| 91 | let rendered = value |
| 92 | .rendered |
| 93 | .iter() |
| 94 | .map(|entry| entry.value.as_str()) |
| 95 | .collect::<Vec<_>>(); |
| 96 | out.push_str(&format!( |
| 97 | "Model-visible tools not in this request: {}\n names: {}\n omitted by render bound: {}\n", |
| 98 | value.count, |
| 99 | serde_json::to_string(&rendered).unwrap_or_else(|_| "unavailable".to_string()), |
| 100 | value.omitted |
| 101 | )); |
| 102 | } |
| 103 | Evidence::Unknown { reason } => { |
| 104 | out.push_str(&format!( |
| 105 | "Model-visible tools not in this request: unknown ({reason})\n" |
| 106 | )); |
| 107 | } |
| 108 | } |
| 109 | out.push_str(&format!( |
| 110 | "Unavailable for this request: {}\n", |
| 111 | snapshot.unavailable_for_this_request.join(", ") |
| 112 | )); |
| 113 | |
| 114 | for tool in &snapshot.tools { |
| 115 | out.push_str(&format!( |
| 116 | "\n{}. {}\n", |
| 117 | tool.ordinal, |
| 118 | json_string(&tool.name.value) |
| 119 | )); |
| 120 | out.push_str(&format!( |
| 121 | " name truncated: {}\n", |
| 122 | yes_no(tool.name.truncated) |
| 123 | )); |
| 124 | render_bounded_evidence(&mut out, "type", &tool.tool_type); |
| 125 | out.push_str(&format!( |
| 126 | " description: {}\n description truncated: {}\n", |
| 127 | json_string(&tool.description.value), |
| 128 | yes_no(tool.description.truncated) |
| 129 | )); |
| 130 | out.push_str(&format!( |
| 131 | " input schema JSON: {}\n input schema truncated: {}\n", |
| 132 | tool.input_schema_json.value, |
| 133 | yes_no(tool.input_schema_json.truncated) |
| 134 | )); |
| 135 | match &tool.allowed_callers { |
| 136 | Evidence::Known { value } => { |
| 137 | let rendered = value |
| 138 | .rendered |
| 139 | .iter() |
| 140 | .map(|entry| entry.value.as_str()) |
| 141 | .collect::<Vec<_>>(); |
| 142 | out.push_str(&format!( |
| 143 | " allowed callers: {}\n allowed callers count: {}\n allowed callers omitted: {}\n allowed callers truncated: {}\n", |
| 144 | serde_json::to_string(&rendered).unwrap_or_else(|_| "unavailable".to_string()), |
| 145 | value.count, |
| 146 | value.omitted, |
| 147 | yes_no(value.rendered.iter().any(|entry| entry.truncated)) |
| 148 | )); |
| 149 | } |
| 150 | Evidence::Unknown { reason } => { |
| 151 | out.push_str(&format!(" allowed callers: unknown ({reason})\n")); |
| 152 | } |
| 153 | } |
| 154 | render_bool_evidence(&mut out, "deferred loading", &tool.defer_loading); |
| 155 | render_bool_evidence(&mut out, "strict", &tool.strict); |
| 156 | match &tool.input_examples { |
| 157 | Evidence::Known { value } => out.push_str(&format!( |
| 158 | " input examples: present ({} value(s), {})\n", |
| 159 | value.count, value.values |
| 160 | )), |
| 161 | Evidence::Unknown { reason } => { |
| 162 | out.push_str(&format!(" input examples: unknown ({reason})\n")); |
| 163 | } |
| 164 | } |
| 165 | render_bounded_evidence(&mut out, "cache control type", &tool.cache_control_type); |
| 166 | out.push_str(&format!( |
| 167 | " request state: {}\n in request: {}\n", |
| 168 | visibility_label(tool.visibility), |
| 169 | yes_no(visibility_in_request(tool.visibility)) |
| 170 | )); |
| 171 | match &tool.provenance { |
| 172 | Evidence::Known { value } => { |
| 173 | out.push_str(&format!(" provenance: {}\n", provenance_label(*value))); |
| 174 | } |
| 175 | Evidence::Unknown { reason } => { |
| 176 | out.push_str(&format!(" provenance: unknown ({reason})\n")); |
| 177 | } |
| 178 | } |
| 179 | render_bounded_evidence(&mut out, "MCP server", &tool.mcp_server); |
| 180 | match &tool.capabilities { |
| 181 | Evidence::Known { value } => { |
| 182 | let rendered = value |
| 183 | .rendered |
| 184 | .iter() |
| 185 | .map(|entry| entry.value.as_str()) |
| 186 | .collect::<Vec<_>>(); |
| 187 | out.push_str(&format!( |
| 188 | " capabilities: {}\n capabilities count: {}\n capabilities omitted: {}\n", |
| 189 | serde_json::to_string(&rendered).unwrap_or_else(|_| "unavailable".to_string()), |
| 190 | value.count, |
| 191 | value.omitted |
| 192 | )); |
| 193 | } |
| 194 | Evidence::Unknown { reason } => { |
| 195 | out.push_str(&format!(" capabilities: unknown ({reason})\n")); |
| 196 | } |
| 197 | } |
| 198 | render_bounded_evidence(&mut out, "approval", &tool.approval); |
| 199 | render_bool_evidence(&mut out, "model visible", &tool.model_visible); |
| 200 | } |
| 201 | out |
| 202 | } |
| 203 | |
| 204 | pub fn render_tool_snapshot_json( |
| 205 | snapshot: &DebugToolSnapshot, |
| 206 | ) -> Result<String, serde_json::Error> { |
| 207 | serde_json::to_string_pretty(snapshot) |
| 208 | } |
| 209 | |
| 210 | const fn visibility_label(value: DebugToolVisibility) -> &'static str { |
| 211 | match value { |
| 212 | DebugToolVisibility::Active => "active", |
| 213 | DebugToolVisibility::Deferred => "deferred", |
| 214 | DebugToolVisibility::InRequest => "in-request", |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | const fn visibility_in_request(value: DebugToolVisibility) -> bool { |
| 219 | matches!( |
| 220 | value, |
| 221 | DebugToolVisibility::Active |
| 222 | | DebugToolVisibility::Deferred |
| 223 | | DebugToolVisibility::InRequest |
| 224 | ) |
| 225 | } |
| 226 | |
| 227 | const fn provenance_label(value: DebugToolProvenance) -> &'static str { |
| 228 | match value { |
| 229 | DebugToolProvenance::Builtin => "builtin", |
| 230 | DebugToolProvenance::Plugin => "plugin", |
| 231 | DebugToolProvenance::Mcp => "mcp", |
| 232 | DebugToolProvenance::Synthetic => "synthetic", |
| 233 | DebugToolProvenance::Unknown => "unknown", |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | fn render_bounded_evidence(out: &mut String, label: &str, evidence: &Evidence<BoundedString>) { |
| 238 | match evidence { |
| 239 | Evidence::Known { value } => out.push_str(&format!( |
| 240 | " {label}: {}\n {label} truncated: {}\n", |
| 241 | json_string(&value.value), |
| 242 | yes_no(value.truncated) |
| 243 | )), |
| 244 | Evidence::Unknown { reason } => { |
| 245 | out.push_str(&format!(" {label}: unknown ({reason})\n")); |
| 246 | } |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | fn render_bool_evidence(out: &mut String, label: &str, evidence: &Evidence<bool>) { |
| 251 | match evidence { |
| 252 | Evidence::Known { value } => out.push_str(&format!(" {label}: {value}\n")), |
| 253 | Evidence::Unknown { reason } => { |
| 254 | out.push_str(&format!(" {label}: unknown ({reason})\n")); |
| 255 | } |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | fn json_string(value: &str) -> String { |
| 260 | serde_json::to_string(value).unwrap_or_else(|_| "\"unavailable\"".to_string()) |
| 261 | } |
| 262 | |
| 263 | fn format_optional_usize(label: &str, value: Option<usize>) -> String { |
| 264 | value.map_or_else( |
| 265 | || format!("{label}: unavailable\n"), |
| 266 | |value| format!("{label}: {value}\n"), |
| 267 | ) |
| 268 | } |
| 269 | |
| 270 | fn format_optional_string(label: &str, value: Option<&str>) -> String { |
| 271 | value.map_or_else( |
| 272 | || format!("{label}: unavailable\n"), |
| 273 | |value| format!("{label}: {value}\n"), |
| 274 | ) |
| 275 | } |
| 276 | |
| 277 | const fn yes_no(value: bool) -> &'static str { |
| 278 | if value { "yes" } else { "no" } |
| 279 | } |
| 280 |