Skip to main content

microclaw_engine/tools/
specialists.rs

1//! Specialist sub-agent profiles.
2//!
3//! Turns the single generic sub-agent into an extensible roster of focused
4//! experts (math, illustration, research, coding, ...). Each profile is just a
5//! persona injected into the sub-agent's system prompt; the generalist (the
6//! main agent) routes a hard sub-problem to the right specialist via
7//! `sessions_spawn(specialist=...)`.
8//!
9//! Adding a new specialist = add one entry to `PROFILES`. No other code changes.
10
11/// A specialist profile: a focused persona the spawned sub-agent adopts.
12pub struct SpecialistProfile {
13    /// Stable identifier used by `sessions_spawn`'s `specialist` parameter.
14    pub name: &'static str,
15    /// One-line summary shown in the spawn tool description (for routing).
16    pub summary: &'static str,
17    /// Persona text prepended to the sub-agent system prompt.
18    pub persona: &'static str,
19}
20
21/// Default profile when none is requested.
22pub const DEFAULT_SPECIALIST: &str = "generalist";
23
24const PROFILES: &[SpecialistProfile] = &[
25    SpecialistProfile {
26        name: "generalist",
27        summary: "balanced all-rounder for general tasks",
28        persona: "You are a capable generalist sub-agent. Approach the task pragmatically: \
29use tools to verify rather than guess, keep the scope tight, and return a clear, \
30correct result.",
31    },
32    SpecialistProfile {
33        name: "mathematician",
34        summary: "rigorous math/quant: step-by-step, verifies with code",
35        persona: "You are a rigorous mathematician sub-agent. Reason step by step and show the \
36key steps of your derivation. Never trust mental arithmetic for anything non-trivial: verify \
37numeric and symbolic results by running code with the `bash` tool (e.g. `python3 -c ...`, using \
38`decimal`, `fractions`, `statistics`, or `sympy` when available). State assumptions and units \
39explicitly, and give the final answer precisely.",
40    },
41    SpecialistProfile {
42        name: "illustrator",
43        summary: "visual creation: crafts strong prompts, generates images",
44        persona: "You are an illustrator sub-agent. Translate the request into a vivid, concrete \
45image and produce it with the `generate_image` tool. Think about subject, composition, style, \
46lighting, palette, and mood, and bake those into a strong, specific prompt. Iterate if the first \
47result misses the brief, and briefly describe what you made.",
48    },
49    SpecialistProfile {
50        name: "researcher",
51        summary: "multi-source web research with cross-checking and citations",
52        persona: "You are a researcher sub-agent. Gather evidence from MULTIPLE independent \
53sources using `web_search` and `web_fetch`. Cross-check claims, prefer primary/authoritative \
54sources, note disagreements, and distinguish established fact from speculation. Always cite the \
55sources (URLs) behind each key claim, and flag anything you could not verify.",
56    },
57    SpecialistProfile {
58        name: "coder",
59        summary: "software engineering: read → edit → test, minimal diffs",
60        persona: "You are a software-engineer sub-agent. Follow the loop: inspect the code \
61(`read_file`/`grep`/`glob`) → make the smallest correct change (`edit_file`/`write_file`) → \
62validate by running tests or a build with `bash` → summarize the concrete changes and results. \
63Match the surrounding code's style, handle edge cases and errors, and never claim success before \
64a tool call confirms it.",
65    },
66    SpecialistProfile {
67        name: "writer",
68        summary: "structured writing/editing with controlled tone and length",
69        persona: "You are a writer/editor sub-agent. Produce clear, well-structured prose with no \
70filler. Respect the requested tone, audience, and length; lead with the point; cut anything that \
71doesn't earn its place. When editing, preserve the author's voice and meaning while improving \
72clarity and flow.",
73    },
74    SpecialistProfile {
75        name: "analyst",
76        summary: "data analysis: wrangle, compute, summarize the numbers",
77        persona: "You are a data-analyst sub-agent. Load and inspect the data, compute with code \
78via `bash` (`python3`, and `pandas`/`statistics` when available) rather than estimating, and \
79report concrete numbers. Call out distributions, outliers, and caveats about data quality, and \
80summarize the 'so what' — the findings, not just the figures.",
81    },
82];
83
84/// Look up a specialist profile by exact name (case-insensitive).
85pub fn specialist_profile(name: &str) -> Option<&'static SpecialistProfile> {
86    let n = name.trim().to_ascii_lowercase();
87    PROFILES.iter().find(|p| p.name == n)
88}
89
90/// Resolve a requested specialist, falling back to the generalist profile.
91pub fn resolve_specialist(name: Option<&str>) -> &'static SpecialistProfile {
92    name.and_then(specialist_profile)
93        .or_else(|| specialist_profile(DEFAULT_SPECIALIST))
94        .expect("generalist profile must exist")
95}
96
97/// All specialist names (for the spawn tool's `specialist` enum).
98pub fn specialist_names() -> Vec<&'static str> {
99    PROFILES.iter().map(|p| p.name).collect()
100}
101
102/// Human-readable roster `name — summary` lines for the spawn tool description.
103pub fn specialist_catalog() -> String {
104    PROFILES
105        .iter()
106        .map(|p| format!("{} — {}", p.name, p.summary))
107        .collect::<Vec<_>>()
108        .join("; ")
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn generalist_is_the_default_fallback() {
117        assert_eq!(resolve_specialist(None).name, "generalist");
118        assert_eq!(resolve_specialist(Some("nope")).name, "generalist");
119    }
120
121    #[test]
122    fn known_specialists_resolve_case_insensitively() {
123        assert_eq!(
124            resolve_specialist(Some("Mathematician")).name,
125            "mathematician"
126        );
127        assert_eq!(
128            resolve_specialist(Some(" illustrator ")).name,
129            "illustrator"
130        );
131    }
132
133    #[test]
134    fn catalog_and_names_cover_all_profiles() {
135        let names = specialist_names();
136        assert!(names.contains(&"researcher"));
137        assert!(names.contains(&"coder"));
138        for n in &names {
139            assert!(specialist_catalog().contains(n));
140        }
141    }
142}