use crate::personality;
use crate::protocol::layers::{
PanoramicAlert, PanoramicInput, PanoramicModule, PanoramicOutput, ReportInput, ReportOutput,
ReportQueryEntry, SavingsInput, SavingsOutput, SavingsSessionRecord,
};
use crate::session::SessionState;
use m1nd_core::error::{M1ndError, M1ndResult};
use std::time::Instant;
pub fn handle_report(state: &mut SessionState, input: ReportInput) -> M1ndResult<ReportOutput> {
let start = Instant::now();
let agent_queries: Vec<_> = state
.query_log
.iter()
.filter(|q| q.agent_id == input.agent_id)
.collect();
let session_queries = agent_queries.len() as u32;
let session_elapsed_ms: f64 = agent_queries.iter().map(|q| q.elapsed_ms).sum();
let queries_answered = session_queries;
let tokens_saved_session = state.savings_tracker.tokens_saved;
let tokens_saved_global = state.global_savings.total_tokens_saved + tokens_saved_session;
let co2_saved_grams = (tokens_saved_global as f64) * 0.0002;
let recent_queries: Vec<ReportQueryEntry> = agent_queries
.iter()
.rev()
.take(10)
.map(|q| ReportQueryEntry {
tool: q.tool.clone(),
query: q.query_preview.clone(),
elapsed_ms: q.elapsed_ms,
m1nd_answered: true,
})
.collect();
let graph = state.graph.read();
let node_count = graph.num_nodes();
let edge_count = graph.num_edges() as u64;
drop(graph);
let uptime = state.uptime_seconds();
let markdown_summary = format!(
"## m1nd Session Report\n\n\
| Metric | Value |\n|---|---|\n\
| Uptime | {:.0}s |\n\
| Queries (this agent) | {} |\n\
| Total elapsed | {:.0}ms |\n\
| Tokens saved (session) | {} |\n\
| Tokens saved (global) | {} |\n\
| CO2 saved | {:.2}g |\n\
| Graph nodes | {} |\n\
| Graph edges | {} |\n\n\
### Recent Queries\n{}",
uptime,
session_queries,
session_elapsed_ms,
tokens_saved_session,
tokens_saved_global,
co2_saved_grams,
node_count,
edge_count,
recent_queries
.iter()
.map(|q| format!("- **{}** `{}` ({:.0}ms)\n", q.tool, q.query, q.elapsed_ms))
.collect::<String>(),
);
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
Ok(ReportOutput {
agent_id: input.agent_id,
session_queries,
session_elapsed_ms,
queries_answered,
tokens_saved_session,
tokens_saved_global,
co2_saved_grams,
recent_queries,
markdown_summary,
})
}
pub fn handle_panoramic(
state: &mut SessionState,
input: PanoramicInput,
) -> M1ndResult<PanoramicOutput> {
let start = Instant::now();
let top_n = (input.top_n as usize).clamp(1, 1000);
let scope = input.scope.as_deref();
let scope_applied = scope.is_some();
let graph = state.graph.read();
let num_nodes = graph.num_nodes() as usize;
if num_nodes == 0 {
return Ok(PanoramicOutput {
modules: vec![],
total_modules: 0,
critical_alerts: vec![],
scope_applied,
elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
});
}
let mut modules: Vec<PanoramicModule> = Vec::new();
for (interned, &nid) in graph.id_to_node.iter() {
let ext_id = graph.strings.resolve(*interned);
if let Some(prefix) = scope {
if !ext_id.contains(prefix) {
continue;
}
}
if !ext_id.starts_with("file::") {
continue;
}
let out_range = graph.csr.out_range(nid);
let in_range = graph.csr.in_range(nid);
let blast_forward = out_range.len() as u32;
let blast_backward = in_range.len() as u32;
let total_edges = (blast_forward + blast_backward) as f32;
let max_possible = if num_nodes > 1 {
(num_nodes - 1) as f32 * 2.0
} else {
1.0
};
let centrality = (total_edges / max_possible).min(1.0);
let churn = 0.0f32;
let blast_normalized =
((blast_forward + blast_backward) as f32 / (num_nodes as f32).max(1.0)).min(1.0);
let combined_risk = blast_normalized * 0.5 + centrality * 0.3 + churn * 0.2;
let is_critical = combined_risk >= 0.7;
let label = ext_id.strip_prefix("file::").unwrap_or(ext_id).to_string();
modules.push(PanoramicModule {
node_id: ext_id.to_string(),
label: label.clone(),
file_path: label,
blast_forward,
blast_backward,
centrality,
combined_risk,
is_critical,
});
}
drop(graph);
modules.sort_by(|a, b| {
b.combined_risk
.partial_cmp(&a.combined_risk)
.unwrap_or(std::cmp::Ordering::Equal)
});
let total_modules = modules.len();
let critical_alerts: Vec<PanoramicAlert> = modules
.iter()
.filter(|m| m.is_critical)
.map(|m| PanoramicAlert {
node_id: m.node_id.clone(),
label: m.label.clone(),
combined_risk: m.combined_risk,
reason: format!(
"high combined risk ({:.2}): blast_fwd={}, blast_bwd={}, centrality={:.2}",
m.combined_risk, m.blast_forward, m.blast_backward, m.centrality
),
})
.collect();
modules.truncate(top_n);
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
Ok(PanoramicOutput {
modules,
total_modules,
critical_alerts,
scope_applied,
elapsed_ms: elapsed,
})
}
pub fn handle_savings(state: &mut SessionState, input: SavingsInput) -> M1ndResult<SavingsOutput> {
let start = Instant::now();
let session_tokens_saved = state.savings_tracker.tokens_saved;
let global_tokens_saved = state.global_savings.total_tokens_saved + session_tokens_saved;
let global_co2_grams = (global_tokens_saved as f64) * 0.0002;
let cost_saved_usd = (global_tokens_saved as f64) * 0.000003;
let session_queries: u32 = state.savings_tracker.queries_by_tool.values().sum::<u64>() as u32;
let session_start_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
- (state.uptime_seconds() * 1000.0) as u64;
let recent_sessions = vec![SavingsSessionRecord {
agent_id: input.agent_id.clone(),
session_start_ms,
queries: session_queries,
tokens_saved: session_tokens_saved,
co2_grams: (session_tokens_saved as f64) * 0.0002,
}];
let formatted_summary = format!(
"{}{} m1nd efficiency report{}\n\n\
{}session:{} {} queries, {} tokens saved\n\
{}global:{} {} tokens saved, ${:.4} USD, {:.2}g CO2\n\n\
{}every query that didn't burn tokens is a gift to the planet.{}\n",
personality::ANSI_BOLD,
personality::ANSI_GREEN,
personality::ANSI_RESET,
personality::ANSI_CYAN,
personality::ANSI_RESET,
session_queries,
session_tokens_saved,
personality::ANSI_GOLD,
personality::ANSI_RESET,
global_tokens_saved,
cost_saved_usd,
global_co2_grams,
personality::ANSI_DIM,
personality::ANSI_RESET,
);
Ok(SavingsOutput {
session_tokens_saved,
global_tokens_saved,
global_co2_grams,
cost_saved_usd,
recent_sessions,
formatted_summary,
})
}