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
165fn collect_project_map_bullets(report: &str, header: &str, limit: usize) -> Vec<String> {
166    let mut in_section = false;
167    let mut bullets = Vec::new();
168
169    for line in report.lines() {
170        let trimmed = line.trim();
171        if trimmed == header {
172            in_section = true;
173            continue;
174        }
175
176        if !in_section {
177            continue;
178        }
179
180        if trimmed.is_empty() {
181            continue;
182        }
183
184        if trimmed.starts_with("-- ")
185            || trimmed == "Likely entrypoints"
186            || trimmed == "Core owner files"
187        {
188            if !bullets.is_empty() {
189                break;
190            }
191            continue;
192        }
193
194        if trimmed.starts_with("- ") {
195            bullets.push(trimmed.to_string());
196            if bullets.len() >= limit {
197                break;
198            }
199            continue;
200        }
201
202        if !trimmed.starts_with("symbols:") {
203            break;
204        }
205    }
206
207    bullets
208}
209
210pub(crate) fn summarize_project_map_output(report: &str) -> String {
211    let mut entrypoints = collect_project_map_bullets(report, "Likely entrypoints", 4);
212    let mut owners = collect_project_map_bullets(report, "Core owner files", 8);
213
214    if owners.is_empty() {
215        let fallback: Vec<String> = report
216            .lines()
217            .map(str::trim)
218            .filter(|line| line.starts_with("- "))
219            .take(8)
220            .map(|line| line.to_string())
221            .collect();
222        owners = fallback;
223    }
224
225    if entrypoints.is_empty() {
226        entrypoints = owners
227            .iter()
228            .filter(|line| line.contains("[entrypoint]"))
229            .take(4)
230            .cloned()
231            .collect();
232    }
233
234    let mut lines =
235        vec!["Based on `map_project`, the grounded architecture summary is:".to_string()];
236
237    if !entrypoints.is_empty() {
238        lines.push(String::new());
239        lines.push("Likely entrypoints".to_string());
240        lines.extend(entrypoints);
241    }
242
243    if !owners.is_empty() {
244        lines.push(String::new());
245        lines.push("Core owner files".to_string());
246        lines.extend(owners);
247    }
248
249    lines.join("\n")
250}
251
252pub(crate) fn summarize_runtime_trace_output(report: &str) -> String {
253    let mut lines = Vec::new();
254    let mut started = false;
255    let mut kept = 0usize;
256
257    for line in report.lines() {
258        let trimmed = line.trim_end();
259        if trimmed.is_empty() {
260            if started && !lines.last().map(|s: &String| s.is_empty()).unwrap_or(false) {
261                lines.push(String::new());
262            }
263            continue;
264        }
265
266        if !started {
267            if trimmed.starts_with("Verified runtime trace")
268                || trimmed.starts_with("Verified runtime subsystems")
269                || trimmed.starts_with("Verified startup flow")
270            {
271                started = true;
272                lines.push(trimmed.to_string());
273            }
274            continue;
275        }
276
277        if trimmed == "Possible weak points" {
278            break;
279        }
280
281        if trimmed.trim_start().starts_with("File refs:") {
282            continue;
283        }
284
285        lines.push(trimmed.to_string());
286        kept += 1;
287
288        if kept >= 24 {
289            break;
290        }
291    }
292
293    lines.join("\n")
294}
295
296pub(crate) fn build_architecture_overview_answer(
297    project_map_summary: &str,
298    runtime_trace_summary: &str,
299) -> String {
300    let mut out = String::new();
301    out.push_str("Grounded architecture overview\n\n");
302    out.push_str("Structure and owner files\n");
303    out.push_str(project_map_summary.trim());
304    out.push_str("\n\nRuntime control flow\n");
305    out.push_str(runtime_trace_summary.trim());
306    out.push_str("\n\nStable workflow contracts\n");
307    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");
308    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");
309    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");
310    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");
311    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");
312    out
313}