Skip to main content

hematite/agent/
diagnose.rs

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