hematite/agent/
architecture_summary.rs1use 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}