1#![forbid(unsafe_code)]
4
5use std::sync::Arc;
6
7use async_trait::async_trait;
8
9use serde_json::{Value, json};
10use wm_core::{Context, EffectRow, Gana, Tool, ToolStats};
11use wm_dispatch::ToolRegistry;
12
13pub struct ToolsUsageReportTool {
20 registry: Arc<ToolRegistry>,
21 stats: ToolStats,
22 effects: EffectRow,
23}
24
25impl ToolsUsageReportTool {
26 #[must_use]
27 pub fn new(registry: Arc<ToolRegistry>) -> Self {
28 Self {
29 registry,
30 stats: ToolStats::default(),
31 effects: EffectRow::pure(),
32 }
33 }
34}
35
36#[async_trait]
37impl Tool for ToolsUsageReportTool {
38 fn name(&self) -> &str {
39 "tools.usage_report"
40 }
41 fn gana(&self) -> Gana {
42 Gana::Ghost
43 }
44 fn effects(&self) -> &EffectRow {
45 &self.effects
46 }
47 fn description(&self) -> &str {
48 "Rank all registered tools by usage from live dispatch stats: calls, success rate, latency, and retirement candidates"
49 }
50 fn input_schema(&self) -> Value {
51 json!({
52 "type": "object",
53 "properties": {
54 "sort": {
55 "type": "string",
56 "enum": ["calls", "effectiveness", "latency"],
57 "description": "Sort key (default: calls, descending; effectiveness ascending — worst first; latency descending — slowest first)"
58 },
59 "limit": {
60 "type": "integer",
61 "description": "Maximum number of tools to return (default: 25, 0 = all)"
62 },
63 "min_calls": {
64 "type": "integer",
65 "description": "Only include tools with at least this many calls (default: 0)"
66 },
67 "retire_candidates_only": {
68 "type": "boolean",
69 "description": "Only include tools flagged for retirement (>= 10 calls, effectiveness < 0.2)"
70 }
71 }
72 })
73 }
74 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
75 let sort = args.get("sort").and_then(Value::as_str).unwrap_or("calls");
76 let limit = args
77 .get("limit")
78 .and_then(Value::as_u64)
79 .map_or(25, |l| l as usize);
80 let min_calls = args.get("min_calls").and_then(Value::as_u64).unwrap_or(0);
81 let retire_only = args
82 .get("retire_candidates_only")
83 .and_then(Value::as_bool)
84 .unwrap_or(false);
85
86 let tools = self.registry.all_ref();
87 let total_registered = tools.len();
88
89 let mut entries: Vec<(u64, f32, u64, Value)> = tools
90 .iter()
91 .filter_map(|t| {
92 let snap = t.stats().snapshot();
93 if snap.call_count < min_calls {
94 return None;
95 }
96 let should_retire = t.stats().should_retire(10, 0.2);
97 if retire_only && !should_retire {
98 return None;
99 }
100 let entry = json!({
101 "name": t.name(),
102 "gana": format!("{:?}", t.gana()),
103 "call_count": snap.call_count,
104 "success_count": snap.success_count,
105 "effectiveness": (f64::from(snap.effectiveness) * 100.0).round() / 100.0,
106 "p50_latency_ms": (snap.p50_latency_ns as f64 / 1_000_000.0 * 100.0).round() / 100.0,
107 "peak_latency_ms": (snap.peak_latency_ns as f64 / 1_000_000.0 * 100.0).round() / 100.0,
108 "last_used_unix": snap.last_used_unix,
109 "should_retire": should_retire,
110 });
111 Some((snap.call_count, snap.effectiveness, snap.p50_latency_ns, entry))
112 })
113 .collect();
114
115 match sort {
116 "effectiveness" => entries.sort_by(|a, b| {
118 a.1.partial_cmp(&b.1)
119 .unwrap_or(std::cmp::Ordering::Equal)
120 .then(b.0.cmp(&a.0))
121 }),
122 "latency" => entries.sort_by_key(|x| std::cmp::Reverse(x.2)),
124 _ => entries.sort_by_key(|x| std::cmp::Reverse(x.0)),
126 }
127
128 let matched = entries.len();
129 let used_count = tools
130 .iter()
131 .filter(|t| {
132 t.stats()
133 .call_count
134 .load(std::sync::atomic::Ordering::Relaxed)
135 > 0
136 })
137 .count();
138 let report: Vec<Value> = entries
139 .into_iter()
140 .map(|(_, _, _, e)| e)
141 .take(if limit == 0 { usize::MAX } else { limit })
142 .collect();
143
144 Ok(json!({
145 "status": "success",
146 "sort": sort,
147 "total_registered": total_registered,
148 "total_used": used_count,
149 "matched": matched,
150 "tools": report,
151 }))
152 }
153 fn stats(&self) -> &ToolStats {
154 &self.stats
155 }
156}
157
158pub struct ToolsEffectivenessReportTool {
159 stats: ToolStats,
160 effects: EffectRow,
161}
162
163impl ToolsEffectivenessReportTool {
164 #[must_use]
165 pub fn new() -> Self {
166 Self {
167 stats: ToolStats::default(),
168 effects: EffectRow::pure(),
169 }
170 }
171}
172
173impl Default for ToolsEffectivenessReportTool {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179#[async_trait]
180impl Tool for ToolsEffectivenessReportTool {
181 fn name(&self) -> &str {
182 "tools.effectiveness_report"
183 }
184 fn gana(&self) -> Gana {
185 Gana::Ghost
186 }
187 fn effects(&self) -> &EffectRow {
188 &self.effects
189 }
190 fn description(&self) -> &str {
191 "Report on tool effectiveness from dispatch stats"
192 }
193 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
194 let total = self
195 .stats
196 .call_count
197 .load(std::sync::atomic::Ordering::Relaxed);
198 let successes = self
199 .stats
200 .success_count
201 .load(std::sync::atomic::Ordering::Relaxed);
202 let failures = total.saturating_sub(successes);
203 let effectiveness = if total > 0 {
204 successes as f32 / total as f32
205 } else {
206 1.0
207 };
208 Ok(json!({
209 "status": "success",
210 "total_calls": total,
211 "successes": successes,
212 "failures": failures,
213 "effectiveness": (effectiveness * 100.0).round() / 100.0,
214 }))
215 }
216 fn stats(&self) -> &ToolStats {
217 &self.stats
218 }
219}
220
221pub struct ToolsRetireTool {
223 stats: ToolStats,
224 effects: EffectRow,
225}
226
227impl ToolsRetireTool {
228 #[must_use]
229 pub fn new() -> Self {
230 Self {
231 stats: ToolStats::default(),
232 effects: EffectRow::pure(),
233 }
234 }
235}
236
237impl Default for ToolsRetireTool {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243#[async_trait]
244impl Tool for ToolsRetireTool {
245 fn name(&self) -> &str {
246 "tools.retire"
247 }
248 fn gana(&self) -> Gana {
249 Gana::Ghost
250 }
251 fn effects(&self) -> &EffectRow {
252 &self.effects
253 }
254 fn description(&self) -> &str {
255 "Check if a tool should be retired based on effectiveness threshold"
256 }
257 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
258 let tool_name = args
259 .get("tool")
260 .and_then(|v| v.as_str())
261 .unwrap_or("unknown");
262 let threshold = args
263 .get("threshold")
264 .and_then(serde_json::Value::as_f64)
265 .unwrap_or(0.10) as f32;
266 let effectiveness = args
267 .get("effectiveness")
268 .and_then(serde_json::Value::as_f64)
269 .unwrap_or(1.0) as f32;
270 let should_retire = effectiveness < threshold;
271 Ok(json!({
272 "status": "success",
273 "tool": tool_name,
274 "effectiveness": effectiveness,
275 "threshold": threshold,
276 "should_retire": should_retire,
277 "recommendation": if should_retire { "retire" } else { "keep" },
278 }))
279 }
280 fn stats(&self) -> &ToolStats {
281 &self.stats
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use std::time::Duration;
289 use wm_dispatch::ToolRegistryBuilder;
290
291 struct StubTool {
292 name: &'static str,
293 stats: ToolStats,
294 effects: EffectRow,
295 }
296
297 impl StubTool {
298 fn new(name: &'static str) -> Self {
299 Self {
300 name,
301 stats: ToolStats::default(),
302 effects: EffectRow::pure(),
303 }
304 }
305 }
306
307 #[async_trait]
308 impl Tool for StubTool {
309 fn name(&self) -> &str {
310 self.name
311 }
312 fn gana(&self) -> Gana {
313 Gana::Ghost
314 }
315 fn effects(&self) -> &EffectRow {
316 &self.effects
317 }
318 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
319 Ok(json!({"status": "success"}))
320 }
321 fn stats(&self) -> &ToolStats {
322 &self.stats
323 }
324 }
325
326 fn registry_with_usage() -> Arc<ToolRegistry> {
327 let hot = Arc::new(StubTool::new("stub.hot"));
328 for _ in 0..20 {
329 hot.stats
330 .record_success(Duration::from_millis(1), Duration::from_millis(1));
331 }
332 let failing = Arc::new(StubTool::new("stub.failing"));
333 for _ in 0..2 {
334 failing
335 .stats
336 .record_success(Duration::from_millis(1), Duration::from_millis(1));
337 }
338 for _ in 0..13 {
339 failing.stats.record_failure(Duration::from_millis(1));
340 }
341 let unused = Arc::new(StubTool::new("stub.unused"));
342
343 let mut builder = ToolRegistryBuilder::new();
344 builder.register(hot);
345 builder.register(failing);
346 builder.register(unused);
347 Arc::new(builder.build())
348 }
349
350 #[tokio::test]
351 async fn usage_report_ranks_by_calls() {
352 let tool = ToolsUsageReportTool::new(registry_with_usage());
353 let mut ctx = Context::default();
354 let out = tool.call(&mut ctx, json!({})).await.unwrap();
355
356 assert_eq!(out["status"], "success");
357 assert_eq!(out["total_registered"], 3);
358 assert_eq!(out["total_used"], 2);
359 let tools = out["tools"].as_array().unwrap();
360 assert_eq!(tools.len(), 3);
361 assert_eq!(tools[0]["name"], "stub.hot");
362 assert_eq!(tools[0]["call_count"], 20);
363 assert_eq!(tools[1]["name"], "stub.failing");
364 }
365
366 #[tokio::test]
367 async fn usage_report_flags_retirement_candidates() {
368 let tool = ToolsUsageReportTool::new(registry_with_usage());
369 let mut ctx = Context::default();
370 let out = tool
371 .call(&mut ctx, json!({"retire_candidates_only": true}))
372 .await
373 .unwrap();
374
375 let tools = out["tools"].as_array().unwrap();
376 assert_eq!(tools.len(), 1);
377 assert_eq!(tools[0]["name"], "stub.failing");
378 assert_eq!(tools[0]["should_retire"], true);
379 }
380
381 #[tokio::test]
382 async fn usage_report_min_calls_and_effectiveness_sort() {
383 let tool = ToolsUsageReportTool::new(registry_with_usage());
384 let mut ctx = Context::default();
385 let out = tool
386 .call(&mut ctx, json!({"min_calls": 1, "sort": "effectiveness"}))
387 .await
388 .unwrap();
389
390 let tools = out["tools"].as_array().unwrap();
391 assert_eq!(tools.len(), 2, "unused tool filtered by min_calls");
392 assert_eq!(
393 tools[0]["name"], "stub.failing",
394 "worst effectiveness first"
395 );
396 }
397}