返回 CodeWhale
tool_snapshot.rs
根目录 / crates / tui / src / diagnostics_reports / tool_snapshot.rs
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
280 lines RUST