Skip to main content

hematite/agent/
architecture_summary.rs

1use crate::agent::inference::ToolCallResponse;
2use serde_json::Value;
3
4fn prompt_mentions_specific_repo_path(user_input: &str) -> bool {
5    let lower = user_input.to_lowercase();
6    lower.contains("src/")
7        || lower.contains("cargo.toml")
8        || lower.contains("readme.md")
9        || lower.contains("memory.md")
10        || lower.contains("claude.md")
11        || lower.contains(".rs")
12        || lower.contains(".py")
13        || lower.contains(".ts")
14        || lower.contains(".js")
15        || lower.contains(".go")
16        || lower.contains(".cs")
17}
18
19fn is_broad_repo_read_tool(name: &str) -> bool {
20    matches!(
21        name,
22        "read_file"
23            | "inspect_lines"
24            | "grep_files"
25            | "list_files"
26            | "auto_pin_context"
27            | "lsp_definitions"
28            | "lsp_references"
29            | "lsp_hover"
30            | "lsp_search_symbol"
31            | "lsp_get_diagnostics"
32    )
33}
34
35pub(crate) fn prune_read_only_context_bloat_batch(
36    calls: Vec<ToolCallResponse>,
37    read_only_mode: bool,
38    architecture_overview_mode: bool,
39) -> (Vec<ToolCallResponse>, Option<String>) {
40    if !read_only_mode || !architecture_overview_mode {
41        return (calls, None);
42    }
43
44    let mut kept = Vec::new();
45    let mut dropped = Vec::new();
46    for call in calls {
47        if matches!(
48            call.function.name.as_str(),
49            "auto_pin_context" | "list_pinned"
50        ) {
51            dropped.push(call.function.name.clone());
52        } else {
53            kept.push(call);
54        }
55    }
56
57    if dropped.is_empty() {
58        return (kept, None);
59    }
60
61    (
62        kept,
63        Some(format!(
64            "Read-only architecture discipline: skipping context-bloat tools in analysis mode (dropped: {}). Use grounded tool output already gathered instead of pinning more files.",
65            dropped.join(", ")
66        )),
67    )
68}
69
70fn trace_topic_priority_for_architecture(call: &ToolCallResponse) -> i32 {
71    let args: Value = serde_json::from_str(&call.function.arguments).unwrap_or(Value::Null);
72    match args.get("topic").and_then(|v| v.as_str()).unwrap_or("") {
73        "runtime_subsystems" => 3,
74        "user_turn" => 2,
75        "startup" => 1,
76        _ => 0,
77    }
78}
79
80pub(crate) fn prune_architecture_trace_batch(
81    calls: Vec<ToolCallResponse>,
82    architecture_overview_mode: bool,
83) -> (Vec<ToolCallResponse>, Option<String>) {
84    if !architecture_overview_mode {
85        return (calls, None);
86    }
87
88    let trace_calls: Vec<_> = calls
89        .iter()
90        .filter(|call| call.function.name == "trace_runtime_flow")
91        .cloned()
92        .collect();
93    if trace_calls.len() <= 1 {
94        return (calls, None);
95    }
96
97    let best_trace = trace_calls
98        .iter()
99        .max_by_key(|call| trace_topic_priority_for_architecture(call))
100        .map(|call| call.id.clone());
101
102    let mut kept = Vec::new();
103    let mut dropped_topics = Vec::new();
104    for call in calls {
105        if call.function.name == "trace_runtime_flow" && Some(call.id.clone()) != best_trace {
106            let args: Value = serde_json::from_str(&call.function.arguments).unwrap_or(Value::Null);
107            let topic = args
108                .get("topic")
109                .and_then(|v| v.as_str())
110                .unwrap_or("unknown");
111            dropped_topics.push(topic.to_string());
112        } else {
113            kept.push(call);
114        }
115    }
116
117    (
118        kept,
119        Some(format!(
120            "Architecture overview discipline: keeping one runtime trace topic for this batch and dropping extra variants (dropped: {}).",
121            dropped_topics.join(", ")
122        )),
123    )
124}
125
126pub(crate) fn prune_authoritative_tool_batch(
127    calls: Vec<ToolCallResponse>,
128    grounded_trace_mode: bool,
129    user_input: &str,
130) -> (Vec<ToolCallResponse>, Option<String>) {
131    if !grounded_trace_mode || prompt_mentions_specific_repo_path(user_input) {
132        return (calls, None);
133    }
134
135    let has_trace = calls
136        .iter()
137        .any(|call| call.function.name == "trace_runtime_flow");
138    if !has_trace {
139        return (calls, None);
140    }
141
142    let mut kept = Vec::new();
143    let mut dropped = Vec::new();
144    for call in calls {
145        if is_broad_repo_read_tool(&call.function.name) {
146            dropped.push(call.function.name.clone());
147        } else {
148            kept.push(call);
149        }
150    }
151
152    if dropped.is_empty() {
153        return (kept, None);
154    }
155
156    (
157        kept,
158        Some(format!(
159            "Runtime-trace discipline: preserving `trace_runtime_flow` as the authoritative runtime source and skipping extra repo reads in the same batch (dropped: {}).",
160            dropped.join(", ")
161        )),
162    )
163}
164
165
166
167pub(crate) fn summarize_runtime_trace_output(report: &str) -> String {
168    let mut lines = Vec::new();
169    let mut started = false;
170    let mut kept = 0usize;
171
172    for line in report.lines() {
173        let trimmed = line.trim_end();
174        if trimmed.is_empty() {
175            if started && !lines.last().map(|s: &String| s.is_empty()).unwrap_or(false) {
176                lines.push(String::new());
177            }
178            continue;
179        }
180
181        if !started {
182            if trimmed.starts_with("Verified runtime trace")
183                || trimmed.starts_with("Verified runtime subsystems")
184                || trimmed.starts_with("Verified startup flow")
185            {
186                started = true;
187                lines.push(trimmed.to_string());
188            }
189            continue;
190        }
191
192        if trimmed == "Possible weak points" {
193            break;
194        }
195
196        if trimmed.trim_start().starts_with("File refs:") {
197            continue;
198        }
199
200        lines.push(trimmed.to_string());
201        kept += 1;
202
203        if kept >= 24 {
204            break;
205        }
206    }
207
208    lines.join("\n")
209}
210
211pub(crate) fn build_architecture_overview_answer(
212    runtime_trace_summary: &str,
213) -> String {
214    let mut out = String::new();
215    out.push_str("Grounded architecture overview\n\n");
216    out.push_str("\n\nRuntime control flow\n");
217    out.push_str(runtime_trace_summary.trim());
218    out.push_str("\n\nStable workflow contracts\n");
219    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");
220    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");
221    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");
222    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");
223    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");
224    out
225}