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