Skip to main content

hematite/agent/
architecture_summary.rs

1use crate::agent::conversation::shell_looks_like_structured_host_inspection;
2use crate::agent::inference::ToolCallResponse;
3use crate::agent::routing::preferred_host_inspection_topic;
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::new();
47    let mut dropped = Vec::new();
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: Value = serde_json::from_str(&call.function.arguments).unwrap_or(Value::Null);
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::new();
105    let mut dropped_topics = Vec::new();
106    for call in calls {
107        if call.function.name == "trace_runtime_flow" && Some(call.id.clone()) != best_trace {
108            let args: Value = serde_json::from_str(&call.function.arguments).unwrap_or(Value::Null);
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::new();
145    let mut dropped = Vec::new();
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::new();
172    let mut dropped_count = 0;
173
174    for call in calls {
175        if call.function.name == "shell" {
176            let args: Value = serde_json::from_str(&call.function.arguments).unwrap_or(Value::Null);
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::new();
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}