Skip to main content

hematite/agent/
diagnose.rs

1/// Staged triage engine for /diagnose. Phase 1 harness, Phase 2 agent synthesis.
2use std::fmt::Write as _;
3
4/// Parse health_report text and return determined follow-up inspect_host topics.
5pub fn triage_follow_up_topics(health_output: &str) -> Vec<&'static str> {
6    let lower = health_output.to_ascii_lowercase();
7    let mut topics: Vec<&'static str> = Vec::with_capacity(12);
8
9    let action_required = lower.contains("action required");
10    let worth_a_look = lower.contains("worth a look");
11    if !action_required && !worth_a_look {
12        return topics; // ALL GOOD — no follow-up needed
13    }
14
15    if lower.contains("[!]") && (lower.contains("disk") || lower.contains("drive")) {
16        topics.push("storage");
17        topics.push("disk_health");
18    } else if lower.contains("[-]") && (lower.contains("disk") || lower.contains("drive")) {
19        topics.push("storage");
20    }
21
22    if (lower.contains("[!]") || lower.contains("[-]")) && lower.contains("ram") {
23        topics.push("resource_load");
24        topics.push("processes");
25    }
26
27    if (lower.contains("critical") || lower.contains("error event")) && lower.contains("event") {
28        topics.push("log_check");
29    }
30
31    if (lower.contains("[!]") || lower.contains("[-]")) && lower.contains("service") {
32        topics.push("services");
33    }
34
35    if (lower.contains("[!]") || lower.contains("[-]"))
36        && (lower.contains("defender") || lower.contains("firewall") || lower.contains("security"))
37    {
38        topics.push("security");
39    }
40
41    if lower.contains("[!]") && lower.contains("internet connectivity") {
42        topics.push("connectivity");
43        topics.push("network");
44    } else if lower.contains("[-]") && lower.contains("internet connectivity") {
45        topics.push("connectivity");
46    }
47
48    if lower.contains("pending reboot") {
49        topics.push("pending_reboot");
50    }
51
52    if (lower.contains("[!]") || lower.contains("[-]"))
53        && (lower.contains("thermal") || lower.contains("°c"))
54    {
55        topics.push("thermal");
56        topics.push("overclocker");
57    }
58
59    let mut seen = std::collections::HashSet::new();
60    topics.retain(|t| seen.insert(*t));
61
62    topics
63}
64
65pub fn fix_follow_up_topics(
66    combined_output: &str,
67    already_ran: &[&str],
68) -> Vec<(&'static str, &'static str)> {
69    let lower = combined_output.to_ascii_lowercase();
70    let ran: std::collections::HashSet<&str> = already_ran.iter().copied().collect();
71    let mut candidates: Vec<(&'static str, &'static str)> = Vec::with_capacity(16);
72    let mut seen = std::collections::HashSet::new();
73
74    macro_rules! add {
75        ($topic:expr, $label:expr, $cond:expr) => {
76            if $cond && !ran.contains($topic) && seen.insert($topic) {
77                candidates.push(($topic, $label));
78            }
79        };
80    }
81
82    add!(
83        "processes",
84        "Top Processes",
85        lower.contains("very high") && (lower.contains("cpu") || lower.contains("processor"))
86    );
87
88    add!(
89        "cpu_power",
90        "CPU Power",
91        lower.contains("throttling") || (lower.contains("very high") && lower.contains("°c"))
92    );
93
94    add!(
95        "app_crashes",
96        "Application Crashes",
97        lower.contains("very high") && lower.contains("memory")
98    );
99
100    add!(
101        "shadow_copies",
102        "Shadow Copies",
103        (lower.contains("unhealthy") || lower.contains("predictive failure"))
104            && lower.contains("disk")
105    );
106
107    add!(
108        "log_check",
109        "Event Log",
110        lower.contains("unexpected shutdown")
111            || lower.contains("kernel: critical")
112            || lower.contains("stop error")
113    );
114
115    add!(
116        "services",
117        "Services",
118        lower.contains("critical/error event")
119            || lower.contains("error events in windows event log")
120    );
121
122    add!(
123        "wifi",
124        "Wi-Fi",
125        lower.contains("unreachable") && !lower.contains("reachable: yes")
126    );
127
128    add!(
129        "connectivity",
130        "Connectivity",
131        lower.contains("dns resolution: failed") || lower.contains("dns: failed")
132    );
133
134    add!(
135        "defender_quarantine",
136        "Defender Quarantine",
137        lower.contains("real-time protection: disabled") || lower.contains("threat detected")
138    );
139
140    add!(
141        "identity_auth",
142        "Identity & Auth",
143        (lower.contains("teams") || lower.contains("outlook"))
144            && (lower.contains("sign-in fail")
145                || lower.contains("auth fail")
146                || lower.contains("token broker"))
147    );
148
149    add!(
150        "credentials",
151        "Credentials",
152        lower.contains("token broker: not running")
153            || lower.contains("wam: not running")
154            || lower.contains("aad broker plugin: not found")
155    );
156
157    candidates.truncate(3);
158    candidates
159}
160
161/// Build the agent instruction for phase 2 of /diagnose.
162pub fn build_diagnose_instruction(health_output: &str, follow_up_topics: &[&str]) -> String {
163    if follow_up_topics.is_empty() {
164        return format!(
165            "DIAGNOSE MODE — triage complete.\n\n\
166             Health report:\n{}\n\n\
167             The machine is in good health. Summarize the key findings for the user \
168             in 2-3 sentences and confirm no action is needed.",
169            health_output
170        );
171    }
172
173    let topic_list = {
174        let mut s = String::with_capacity(follow_up_topics.len() * 40);
175        for (i, t) in follow_up_topics.iter().enumerate() {
176            if i > 0 {
177                s.push('\n');
178            }
179            let _ = write!(s, "{}. inspect_host(topic=\"{}\")", i + 1, t);
180        }
181        s
182    };
183
184    format!(
185        "DIAGNOSE MODE — harness triage identified {} area(s) to investigate.\n\n\
186         Health report (already run by harness):\n{}\n\n\
187         PROTOCOL — follow this exactly:\n\
188         Call each topic below in order:\n{}\n\n\
189         After all calls complete:\n\
190         - Write a numbered fix plan grounded in the tool output\n\
191         - Lead with the most critical issue first\n\
192         - Every step must reference specific data from the results (exact path, count, service name, etc.)\n\
193         - No generic advice — only steps that address what the tools actually found\n\
194         - If a finding needs a restart or elevated privileges, say so explicitly",
195        follow_up_topics.len(),
196        health_output,
197        topic_list
198    )
199}