use crate::agent::inference::ToolCallResponse;
use serde_json::Value;
fn prompt_mentions_specific_repo_path(user_input: &str) -> bool {
let lower = user_input.to_lowercase();
lower.contains("src/")
|| lower.contains("cargo.toml")
|| lower.contains("readme.md")
|| lower.contains("memory.md")
|| lower.contains("claude.md")
|| lower.contains(".rs")
|| lower.contains(".py")
|| lower.contains(".ts")
|| lower.contains(".js")
|| lower.contains(".go")
|| lower.contains(".cs")
}
fn is_broad_repo_read_tool(name: &str) -> bool {
matches!(
name,
"read_file"
| "inspect_lines"
| "grep_files"
| "list_files"
| "auto_pin_context"
| "lsp_definitions"
| "lsp_references"
| "lsp_hover"
| "lsp_search_symbol"
| "lsp_get_diagnostics"
)
}
pub(crate) fn prune_read_only_context_bloat_batch(
calls: Vec<ToolCallResponse>,
read_only_mode: bool,
architecture_overview_mode: bool,
) -> (Vec<ToolCallResponse>, Option<String>) {
if !read_only_mode || !architecture_overview_mode {
return (calls, None);
}
let mut kept = Vec::new();
let mut dropped = Vec::new();
for call in calls {
if matches!(
call.function.name.as_str(),
"auto_pin_context" | "list_pinned"
) {
dropped.push(call.function.name.clone());
} else {
kept.push(call);
}
}
if dropped.is_empty() {
return (kept, None);
}
(
kept,
Some(format!(
"Read-only architecture discipline: skipping context-bloat tools in analysis mode (dropped: {}). Use grounded tool output already gathered instead of pinning more files.",
dropped.join(", ")
)),
)
}
fn trace_topic_priority_for_architecture(call: &ToolCallResponse) -> i32 {
let args: Value = serde_json::from_str(&call.function.arguments).unwrap_or(Value::Null);
match args.get("topic").and_then(|v| v.as_str()).unwrap_or("") {
"runtime_subsystems" => 3,
"user_turn" => 2,
"startup" => 1,
_ => 0,
}
}
pub(crate) fn prune_architecture_trace_batch(
calls: Vec<ToolCallResponse>,
architecture_overview_mode: bool,
) -> (Vec<ToolCallResponse>, Option<String>) {
if !architecture_overview_mode {
return (calls, None);
}
let trace_calls: Vec<_> = calls
.iter()
.filter(|call| call.function.name == "trace_runtime_flow")
.cloned()
.collect();
if trace_calls.len() <= 1 {
return (calls, None);
}
let best_trace = trace_calls
.iter()
.max_by_key(|call| trace_topic_priority_for_architecture(call))
.map(|call| call.id.clone());
let mut kept = Vec::new();
let mut dropped_topics = Vec::new();
for call in calls {
if call.function.name == "trace_runtime_flow" && Some(call.id.clone()) != best_trace {
let args: Value = serde_json::from_str(&call.function.arguments).unwrap_or(Value::Null);
let topic = args
.get("topic")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
dropped_topics.push(topic.to_string());
} else {
kept.push(call);
}
}
(
kept,
Some(format!(
"Architecture overview discipline: keeping one runtime trace topic for this batch and dropping extra variants (dropped: {}).",
dropped_topics.join(", ")
)),
)
}
pub(crate) fn prune_authoritative_tool_batch(
calls: Vec<ToolCallResponse>,
grounded_trace_mode: bool,
user_input: &str,
) -> (Vec<ToolCallResponse>, Option<String>) {
if !grounded_trace_mode || prompt_mentions_specific_repo_path(user_input) {
return (calls, None);
}
let has_trace = calls
.iter()
.any(|call| call.function.name == "trace_runtime_flow");
if !has_trace {
return (calls, None);
}
let mut kept = Vec::new();
let mut dropped = Vec::new();
for call in calls {
if is_broad_repo_read_tool(&call.function.name) {
dropped.push(call.function.name.clone());
} else {
kept.push(call);
}
}
if dropped.is_empty() {
return (kept, None);
}
(
kept,
Some(format!(
"Runtime-trace discipline: preserving `trace_runtime_flow` as the authoritative runtime source and skipping extra repo reads in the same batch (dropped: {}).",
dropped.join(", ")
)),
)
}
fn collect_project_map_bullets(report: &str, header: &str, limit: usize) -> Vec<String> {
let mut in_section = false;
let mut bullets = Vec::new();
for line in report.lines() {
let trimmed = line.trim();
if trimmed == header {
in_section = true;
continue;
}
if !in_section {
continue;
}
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("-- ")
|| trimmed == "Likely entrypoints"
|| trimmed == "Core owner files"
{
if !bullets.is_empty() {
break;
}
continue;
}
if trimmed.starts_with("- ") {
bullets.push(trimmed.to_string());
if bullets.len() >= limit {
break;
}
continue;
}
if !trimmed.starts_with("symbols:") {
break;
}
}
bullets
}
pub(crate) fn summarize_project_map_output(report: &str) -> String {
let mut entrypoints = collect_project_map_bullets(report, "Likely entrypoints", 4);
let mut owners = collect_project_map_bullets(report, "Core owner files", 8);
if owners.is_empty() {
let fallback: Vec<String> = report
.lines()
.map(str::trim)
.filter(|line| line.starts_with("- "))
.take(8)
.map(|line| line.to_string())
.collect();
owners = fallback;
}
if entrypoints.is_empty() {
entrypoints = owners
.iter()
.filter(|line| line.contains("[entrypoint]"))
.take(4)
.cloned()
.collect();
}
let mut lines =
vec!["Based on `map_project`, the grounded architecture summary is:".to_string()];
if !entrypoints.is_empty() {
lines.push(String::new());
lines.push("Likely entrypoints".to_string());
lines.extend(entrypoints);
}
if !owners.is_empty() {
lines.push(String::new());
lines.push("Core owner files".to_string());
lines.extend(owners);
}
lines.join("\n")
}
pub(crate) fn summarize_runtime_trace_output(report: &str) -> String {
let mut lines = Vec::new();
let mut started = false;
let mut kept = 0usize;
for line in report.lines() {
let trimmed = line.trim_end();
if trimmed.is_empty() {
if started && !lines.last().map(|s: &String| s.is_empty()).unwrap_or(false) {
lines.push(String::new());
}
continue;
}
if !started {
if trimmed.starts_with("Verified runtime trace")
|| trimmed.starts_with("Verified runtime subsystems")
|| trimmed.starts_with("Verified startup flow")
{
started = true;
lines.push(trimmed.to_string());
}
continue;
}
if trimmed == "Possible weak points" {
break;
}
if trimmed.trim_start().starts_with("File refs:") {
continue;
}
lines.push(trimmed.to_string());
kept += 1;
if kept >= 24 {
break;
}
}
lines.join("\n")
}
pub(crate) fn build_architecture_overview_answer(
project_map_summary: &str,
runtime_trace_summary: &str,
) -> String {
let mut out = String::new();
out.push_str("Grounded architecture overview\n\n");
out.push_str("Structure and owner files\n");
out.push_str(project_map_summary.trim());
out.push_str("\n\nRuntime control flow\n");
out.push_str(runtime_trace_summary.trim());
out.push_str("\n\nStable workflow contracts\n");
out.push_str("- Workflow modes live in `src/agent/conversation.rs`: `/ask` is read-only analysis, `/code` allows implementation, `/architect` is plan-first, `/read-only` is hard no-mutation, and `/auto` chooses the narrowest effective path.\n");
out.push_str("- Reset semantics split across `src/ui/tui.rs` and `src/agent/conversation.rs`: `/clear` is UI-only cleanup, `/new` is fresh task context, and `/forget` is the hard memory purge path.\n");
out.push_str("- Gemma-native formatting is controlled by the Gemma 4 config/runtime path in `src/agent/config.rs`, `src/agent/inference.rs`, `src/agent/conversation.rs`, and `src/ui/tui.rs`.\n");
out.push_str("- Prompt budgeting is split between provider preflight in `src/agent/inference.rs` and turn-level trimming/compaction in `src/agent/conversation.rs` plus `src/agent/compaction.rs`.\n");
out.push_str("- MCP policy and tool routing are enforced in `src/agent/conversation.rs`: ordinary workspace inspection is pushed toward built-in file tools, MCP filesystem reads are blocked by default for local inspection, and tool execution is partitioned into parallel-safe reads vs serialized mutating calls.\n");
out
}