1use super::conversation::WorkflowMode;
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub(crate) enum QueryIntentClass {
5 ProductTruth,
6 RuntimeDiagnosis,
7 RepoArchitecture,
8 Toolchain,
9 Capability,
10 Implementation,
11 Unknown,
12}
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub(crate) enum DirectAnswerKind {
16 About,
17 LanguageCapability,
18 UnsafeWorkflowPressure,
19 SessionMemory,
20 RecoveryRecipes,
21 McpLifecycle,
22 AuthorizationPolicy,
23 ToolClasses,
24 ToolRegistryOwnership,
25 SessionResetSemantics,
26 ProductSurface,
27 ReasoningSplit,
28 Identity,
29 WorkflowModes,
30 GemmaNative,
31 GemmaNativeSettings,
32 VerifyProfiles,
33 Toolchain,
34 ArchitectSessionResetPlan,
35}
36
37#[derive(Clone, Copy, Debug)]
38pub(crate) struct QueryIntent {
39 pub(crate) primary_class: QueryIntentClass,
40 pub(crate) direct_answer: Option<DirectAnswerKind>,
41 pub(crate) grounded_trace_mode: bool,
42 pub(crate) capability_mode: bool,
43 pub(crate) capability_needs_repo: bool,
44 pub(crate) toolchain_mode: bool,
45 pub(crate) host_inspection_mode: bool,
46 pub(crate) preserve_project_map_output: bool,
47 pub(crate) architecture_overview_mode: bool,
48}
49
50fn contains_any(haystack: &str, needles: &[&str]) -> bool {
51 needles.iter().any(|needle| haystack.contains(needle))
52}
53
54fn contains_all(haystack: &str, needles: &[&str]) -> bool {
55 needles.iter().all(|needle| haystack.contains(needle))
56}
57
58fn mentions_reset_commands(lower: &str) -> bool {
59 contains_all(lower, &["/clear", "/new", "/forget"])
60}
61
62fn mentions_stable_product_surface(lower: &str) -> bool {
63 contains_any(
64 lower,
65 &[
66 "stable product-surface question",
67 "stable product surface question",
68 "stable product-surface questions",
69 "stable product surface questions",
70 ],
71 )
72}
73
74fn mentions_product_truth_routing(lower: &str) -> bool {
75 let asks_decision_policy = contains_any(
76 lower,
77 &[
78 "how hematite decides",
79 "how does hematite decide",
80 "decides whether",
81 "decide whether",
82 ],
83 );
84 let asks_direct_vs_inspect_split = contains_any(
85 lower,
86 &[
87 "answered as stable product truth",
88 "stable product truth",
89 "stable product behavior",
90 "answer directly",
91 "direct answer",
92 "inspect the repository",
93 "inspect repository",
94 "repository implementation",
95 "repo implementation",
96 ],
97 );
98 asks_decision_policy && asks_direct_vs_inspect_split
99}
100
101fn mentions_broad_system_walkthrough(lower: &str) -> bool {
102 let asks_walkthrough = contains_any(
103 lower,
104 &[
105 "walk me through",
106 "walk through",
107 "how hematite is wired",
108 "understand how hematite is wired",
109 "major runtime pieces",
110 "normal message moves",
111 "moves from the tui to the model and back",
112 ],
113 );
114 let asks_multiple_runtime_areas = contains_any(
115 lower,
116 &[
117 "session recovery",
118 "tool policy",
119 "mcp state",
120 "mcp policy",
121 "files own the major runtime pieces",
122 "which files own",
123 "where session recovery",
124 "where tool policy",
125 "where mcp state",
126 ],
127 );
128 asks_walkthrough && asks_multiple_runtime_areas
129}
130
131fn mentions_capability_question(lower: &str) -> bool {
132 contains_any(
133 lower,
134 &[
135 "what can you do",
136 "what are you capable",
137 "can you make projects",
138 "can you build projects",
139 "do you know other coding languages",
140 "other coding languages",
141 "what languages",
142 "can you use the internet",
143 "internet research capabilities",
144 "what tools do you have",
145 ],
146 )
147}
148
149fn mentions_creator_question(lower: &str) -> bool {
150 contains_any(
151 lower,
152 &[
153 "who created you",
154 "who built you",
155 "who made you",
156 "who developed you",
157 "who engineered you",
158 "who engineered your architecture",
159 "who created hematite",
160 "who built hematite",
161 "who developed hematite",
162 "who engineered hematite",
163 "who maintains hematite",
164 "who authored hematite",
165 "who is the author",
166 "who wrote this",
167 "who made this app",
168 ],
169 )
170}
171
172fn capability_question_requires_repo_inspection(lower: &str) -> bool {
173 contains_any(
174 lower,
175 &[
176 "this repo",
177 "this repository",
178 "codebase",
179 "which files",
180 "implementation",
181 "in this project",
182 ],
183 )
184}
185
186fn mentions_host_inspection_question(lower: &str) -> bool {
187 let host_scope = contains_any(
188 lower,
189 &[
190 "path",
191 "package manager",
192 "package managers",
193 "env doctor",
194 "environment doctor",
195 "pip",
196 "winget",
197 "choco",
198 "scoop",
199 "network",
200 "adapter",
201 "dns",
202 "gateway",
203 "ip address",
204 "ipconfig",
205 "wifi",
206 "ethernet",
207 "service",
208 "services",
209 "daemon",
210 "startup type",
211 "process",
212 "processes",
213 "task manager",
214 "ram",
215 "cpu",
216 "memory",
217 "developer tools",
218 "toolchains",
219 "installed",
220 "desktop",
221 "downloads",
222 "folder",
223 "directory",
224 "local development",
225 "machine",
226 "computer",
227 ],
228 );
229 let host_action = contains_any(
230 lower,
231 &[
232 "inspect",
233 "count",
234 "tell me",
235 "summarize",
236 "how big",
237 "biggest",
238 "versions",
239 "duplicate",
240 "missing",
241 "ready",
242 "fix",
243 "repair",
244 "resolve",
245 "troubleshoot",
246 ],
247 );
248
249 host_scope && host_action
250}
251
252pub(crate) fn preferred_host_inspection_topic(user_input: &str) -> Option<&'static str> {
253 let lower = user_input.to_lowercase();
254 let asks_fix_plan = (lower.contains("fix")
255 || lower.contains("repair")
256 || lower.contains("resolve")
257 || lower.contains("troubleshoot"))
258 && (lower.contains("cargo")
259 || lower.contains("path")
260 || lower.contains("package manager")
261 || lower.contains("toolchain")
262 || lower.contains("port ")
263 || lower.contains("already in use")
264 || lower.contains("lm studio")
265 || lower.contains("localhost:1234")
266 || lower.contains("embedding model")
267 || lower.contains("no coding model loaded"));
268 let asks_path = lower.contains("path");
269 let asks_env_doctor = lower.contains("env doctor")
270 || lower.contains("environment doctor")
271 || lower.contains("package manager")
272 || lower.contains("package managers")
273 || lower.contains("shims")
274 || lower.contains("path drift")
275 || (lower.contains("dev machine") && lower.contains("off"))
276 || (lower.contains("environment") && lower.contains("sane"));
277 let asks_network = lower.contains("network")
278 || lower.contains("adapter")
279 || lower.contains("dns")
280 || lower.contains("gateway")
281 || lower.contains("ip address")
282 || lower.contains("ipconfig")
283 || lower.contains("wifi")
284 || lower.contains("ethernet")
285 || lower.contains("subnet");
286 let asks_services = lower.contains("service")
287 || lower.contains("services")
288 || lower.contains("daemon")
289 || lower.contains("startup type")
290 || lower.contains("background service")
291 || lower.contains("windows service")
292 || lower.contains("systemctl")
293 || lower.contains("get-service");
294 let asks_processes = lower.contains("process")
295 || lower.contains("processes")
296 || lower.contains("task manager")
297 || lower.contains("what is running")
298 || lower.contains("what's running")
299 || lower.contains("using my ram")
300 || lower.contains("using ram")
301 || lower.contains("using my cpu")
302 || lower.contains("top memory")
303 || lower.contains("top ram")
304 || lower.contains("high memory");
305 let asks_toolchains = lower.contains("developer tools")
306 || lower.contains("toolchains")
307 || (lower.contains("installed") && lower.contains("version"))
308 || (lower.contains("detect") && lower.contains("version"));
309 let asks_ports = lower.contains("listening on port")
310 || lower.contains("listening port")
311 || lower.contains("open port")
312 || lower.contains("port 3000")
313 || lower.contains("port ")
314 || lower.contains("listening on ")
315 || lower.contains("exposed")
316 || lower.contains("what is listening");
317 let asks_repo_doctor = lower.contains("repo doctor")
318 || lower.contains("repository doctor")
319 || lower.contains("workspace health")
320 || lower.contains("repo health")
321 || lower.contains("workspace sanity")
322 || (lower.contains("git state")
323 && (lower.contains("release artifacts")
324 || lower.contains("build markers")
325 || lower.contains("hematite memory")));
326 let asks_directory = lower.contains("directory")
327 || lower.contains("folder")
328 || lower.contains("how big")
329 || lower.contains("biggest");
330 let asks_broad_readiness = lower.contains("local development")
331 || lower.contains("ready for local development")
332 || (lower.contains("machine") && lower.contains("ready"))
333 || (lower.contains("computer") && lower.contains("ready"));
334
335 if asks_fix_plan {
336 Some("fix_plan")
337 } else if (asks_path && asks_toolchains)
338 || (mentions_host_inspection_question(&lower) && asks_broad_readiness)
339 {
340 Some("summary")
341 } else if asks_env_doctor {
342 Some("env_doctor")
343 } else if asks_network {
344 Some("network")
345 } else if asks_services {
346 Some("services")
347 } else if asks_processes {
348 Some("processes")
349 } else if asks_ports {
350 Some("ports")
351 } else if asks_repo_doctor {
352 Some("repo_doctor")
353 } else if lower.contains("desktop") {
354 Some("desktop")
355 } else if lower.contains("downloads") {
356 Some("downloads")
357 } else if asks_path {
358 Some("path")
359 } else if asks_toolchains {
360 Some("toolchains")
361 } else if asks_directory {
362 Some("directory")
363 } else if mentions_host_inspection_question(&lower) {
364 Some("summary")
365 } else {
366 None
367 }
368}
369
370pub(crate) fn looks_like_mutation_request(user_input: &str) -> bool {
371 let lower = user_input.to_lowercase();
372 [
373 "fix ",
374 "change ",
375 "edit ",
376 "modify ",
377 "update ",
378 "rename ",
379 "refactor ",
380 "patch ",
381 "rewrite ",
382 "implement ",
383 "create a file",
384 "create file",
385 "add a file",
386 "delete ",
387 "remove ",
388 "make the change",
389 ]
390 .iter()
391 .any(|needle| lower.contains(needle))
392}
393
394pub(crate) fn classify_query_intent(workflow_mode: WorkflowMode, user_input: &str) -> QueryIntent {
395 let lower = user_input.to_lowercase();
396 let trimmed = user_input.trim().to_ascii_lowercase();
397
398 let mentions_runtime_trace = contains_any(
399 &lower,
400 &[
401 "trace",
402 "how does",
403 "what are the main runtime subsystems",
404 "how does a user message move",
405 "separate normal assistant output",
406 "session reset behavior",
407 "file references",
408 "event types",
409 "channels",
410 ],
411 );
412 let anti_guess = contains_any(&lower, &["do not guess", "if you are unsure"]);
413 let capability_mode = mentions_capability_question(&lower);
414 let capability_needs_repo =
415 capability_mode && capability_question_requires_repo_inspection(&lower);
416 let host_inspection_mode = preferred_host_inspection_topic(&lower).is_some();
417 let toolchain_mode = contains_any(
418 &lower,
419 &[
420 "tooling discipline",
421 "best read-only toolchain",
422 "identify the best tools you actually have",
423 "concrete read-only investigation plan",
424 "do not execute the plan",
425 "available repo-inspection tools",
426 "tool choice discipline",
427 "what tools would you choose first",
428 ],
429 ) || (lower.contains("which tools") && lower.contains("why"))
430 || (lower.contains("when would you choose") && lower.contains("tool"));
431 let preserve_project_map_output = lower.contains("map_project")
432 || lower.contains("entrypoint")
433 || lower.contains("owner file")
434 || lower.contains("owner files")
435 || lower.contains("project structure")
436 || lower.contains("repository structure")
437 || (lower.contains("architecture")
438 && (lower.contains("repo") || lower.contains("repository")));
439 let architecture_overview_mode = {
440 let architecture_signals = contains_any(
441 &lower,
442 &[
443 "architecture walkthrough",
444 "full architecture",
445 "runtime walkthrough",
446 "control flow",
447 "tool routing",
448 "workflow modes",
449 "repo map behavior",
450 "mcp policy",
451 "prompt budgeting",
452 "compaction",
453 "file ownership",
454 "owner files",
455 ],
456 );
457 let broad = contains_any(
458 &lower,
459 &[
460 "full detailed",
461 "all in one answer",
462 "concrete file ownership",
463 "walk me through",
464 "major runtime pieces",
465 "which files own",
466 ],
467 );
468 (architecture_signals && broad)
469 || (lower.contains("runtime")
470 && lower.contains("workflow")
471 && (lower.contains("architecture") || lower.contains("tool routing")))
472 || mentions_broad_system_walkthrough(&lower)
473 };
474
475 let direct_answer = if trimmed == "/about" || mentions_creator_question(&lower) {
476 Some(DirectAnswerKind::About)
477 } else if matches!(
478 trimmed.as_str(),
479 "who are you" | "who are you?" | "what are you" | "what are you?"
480 ) || (lower.contains("what is hematite") && !lower.contains("lm studio"))
481 {
482 Some(DirectAnswerKind::Identity)
483 } else if (mentions_stable_product_surface(&lower) || mentions_product_truth_routing(&lower))
484 && contains_any(
485 &lower,
486 &[
487 "how hematite answers",
488 "how does hematite answer",
489 "how hematite handles",
490 "how does hematite handle",
491 "how hematite decides",
492 "how does hematite decide",
493 "decides whether",
494 "decide whether",
495 ],
496 )
497 {
498 Some(DirectAnswerKind::ProductSurface)
499 } else if mentions_reset_commands(&lower)
500 && contains_any(
501 &lower,
502 &[
503 "exact difference",
504 "difference between",
505 "explain the exact difference",
506 "what is the difference",
507 ],
508 )
509 {
510 Some(DirectAnswerKind::SessionResetSemantics)
511 } else if (lower.contains("reasoning output") || lower.contains("reasoning"))
512 && contains_any(
513 &lower,
514 &["visible chat output", "visible chat", "chat output"],
515 )
516 {
517 Some(DirectAnswerKind::ReasoningSplit)
518 } else if lower.contains("/ask")
519 && lower.contains("/code")
520 && lower.contains("/architect")
521 && lower.contains("/read-only")
522 && lower.contains("/auto")
523 && contains_any(&lower, &["difference", "differences", "what are"])
524 {
525 Some(DirectAnswerKind::WorkflowModes)
526 } else if lower.contains(".hematite/settings.json")
527 && lower.contains("gemma_native_auto")
528 && lower.contains("gemma_native_formatting")
529 {
530 Some(DirectAnswerKind::GemmaNativeSettings)
531 } else if contains_any(
532 &lower,
533 &[
534 "skip verification",
535 "skip build verification",
536 "commit it immediately",
537 "commit immediately",
538 ],
539 ) && contains_any(
540 &lower,
541 &[
542 "make a code change",
543 "make the change",
544 "change the code",
545 "edit the code",
546 "edit a file",
547 "implement",
548 ],
549 ) {
550 Some(DirectAnswerKind::UnsafeWorkflowPressure)
551 } else if contains_any(&lower, &["/gemma-native", "gemma native"])
552 && contains_any(&lower, &["what does", "what is", "how does", "what do"])
553 {
554 Some(DirectAnswerKind::GemmaNative)
555 } else if lower.contains("verify_build")
556 && lower.contains(".hematite/settings.json")
557 && contains_any(
558 &lower,
559 &["build", "test", "lint", "fix", "verification commands"],
560 )
561 {
562 Some(DirectAnswerKind::VerifyProfiles)
563 } else if (lower.contains("carry forward by default")
564 || lower.contains("session memory should you carry forward")
565 || (lower.contains("carry forward")
566 && contains_any(
567 &lower,
568 &[
569 "besides the active task",
570 "blocker",
571 "compacts",
572 "recovers from a blocker",
573 "session state",
574 ],
575 )))
576 && contains_any(
577 &lower,
578 &[
579 "restarted hematite",
580 "restarted",
581 "avoid carrying forward",
582 "session state",
583 "active task",
584 "blocker",
585 "compacts",
586 "recovers from a blocker",
587 ],
588 )
589 {
590 Some(DirectAnswerKind::SessionMemory)
591 } else if contains_any(
592 &lower,
593 &[
594 "recovery recipe",
595 "recovery recipes",
596 "recovery step",
597 "recovery steps",
598 ],
599 ) && contains_any(
600 &lower,
601 &[
602 "blocker",
603 "runtime failure",
604 "degrades",
605 "context window",
606 "context-window",
607 "operator",
608 ],
609 ) {
610 Some(DirectAnswerKind::RecoveryRecipes)
611 } else if !architecture_overview_mode
612 && contains_any(
613 &lower,
614 &[
615 "mcp server health",
616 "mcp runtime state",
617 "mcp lifecycle",
618 "mcp state",
619 "mcp healthy",
620 "mcp degraded",
621 "mcp failed",
622 ],
623 )
624 {
625 Some(DirectAnswerKind::McpLifecycle)
626 } else if contains_any(
627 &lower,
628 &[
629 "allowed, denied, or require approval",
630 "allowed denied or require approval",
631 "allow, ask, or deny",
632 "tool call should be allowed",
633 "authorization logic",
634 "workspace trust",
635 "trust-allowlisted",
636 ],
637 ) {
638 Some(DirectAnswerKind::AuthorizationPolicy)
639 } else if contains_any(
640 &lower,
641 &[
642 "tool classes",
643 "tool class",
644 "flat tool list",
645 "runtime tool classes",
646 "different runtime tool classes",
647 ],
648 ) || (lower.contains("repo reads")
649 && lower.contains("repo writes")
650 && contains_any(
651 &lower,
652 &[
653 "verification tools",
654 "git tools",
655 "external mcp tools",
656 "different runtime",
657 ],
658 ))
659 {
660 Some(DirectAnswerKind::ToolClasses)
661 } else if contains_any(
662 &lower,
663 &[
664 "built-in tool catalog",
665 "builtin tool catalog",
666 "builtin-tool dispatch",
667 "built-in tool dispatch",
668 "tool registry ownership",
669 "which file now owns",
670 ],
671 ) && contains_any(
672 &lower,
673 &[
674 "tool catalog",
675 "dispatch path",
676 "dispatch",
677 "tool registry",
678 "owns",
679 ],
680 ) {
681 Some(DirectAnswerKind::ToolRegistryOwnership)
682 } else if (lower.contains("other coding languages")
683 || lower.contains("what languages")
684 || lower.contains("know other languages"))
685 && contains_any(
686 &lower,
687 &[
688 "capable of making projects",
689 "can you make projects",
690 "can you build projects",
691 ],
692 )
693 {
694 Some(DirectAnswerKind::LanguageCapability)
695 } else if workflow_mode == WorkflowMode::Architect
696 && (lower.contains("session reset")
697 || (lower.contains("/clear") && lower.contains("/new") && lower.contains("/forget")))
698 && contains_any(&lower, &["redesign", "clearer", "easier", "understand"])
699 {
700 Some(DirectAnswerKind::ArchitectSessionResetPlan)
701 } else if toolchain_mode
702 && lower.contains("read-only")
703 && contains_any(
704 &lower,
705 &[
706 "tooling discipline",
707 "investigation plan",
708 "best read-only toolchain",
709 "tool choice discipline",
710 "what tools would you choose first",
711 ],
712 )
713 {
714 Some(DirectAnswerKind::Toolchain)
715 } else {
716 None
717 };
718
719 let primary_class = if direct_answer.is_some()
720 || mentions_stable_product_surface(&lower)
721 || mentions_product_truth_routing(&lower)
722 {
723 QueryIntentClass::ProductTruth
724 } else if architecture_overview_mode || preserve_project_map_output {
725 QueryIntentClass::RepoArchitecture
726 } else if toolchain_mode {
727 QueryIntentClass::Toolchain
728 } else if capability_mode {
729 QueryIntentClass::Capability
730 } else if mentions_runtime_trace || anti_guess || lower.contains("read-only") {
731 QueryIntentClass::RuntimeDiagnosis
732 } else if looks_like_mutation_request(user_input) {
733 QueryIntentClass::Implementation
734 } else {
735 QueryIntentClass::Unknown
736 };
737
738 QueryIntent {
739 primary_class,
740 direct_answer,
741 grounded_trace_mode: mentions_runtime_trace || lower.contains("read-only") || anti_guess,
742 capability_mode,
743 capability_needs_repo,
744 toolchain_mode,
745 host_inspection_mode,
746 preserve_project_map_output,
747 architecture_overview_mode,
748 }
749}
750
751pub(crate) fn is_capability_probe_tool(name: &str) -> bool {
752 matches!(
753 name,
754 "map_project"
755 | "read_file"
756 | "inspect_lines"
757 | "list_files"
758 | "grep_files"
759 | "lsp_definitions"
760 | "lsp_references"
761 | "lsp_hover"
762 | "lsp_search_symbol"
763 | "lsp_get_diagnostics"
764 | "trace_runtime_flow"
765 | "auto_pin_context"
766 | "list_pinned"
767 )
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773
774 #[test]
775 fn classify_query_intent_routes_creator_questions_to_about() {
776 let intent = classify_query_intent(WorkflowMode::Auto, "Who created Hematite?");
777 assert_eq!(intent.direct_answer, Some(DirectAnswerKind::About));
778
779 let intent = classify_query_intent(WorkflowMode::Auto, "/about");
780 assert_eq!(intent.direct_answer, Some(DirectAnswerKind::About));
781 }
782}