Skip to main content

codetether_agent/autochat/
mod.rs

1//! Shared autochat relay helpers used by TUI and CLI flows.
2
3pub mod model_rotation;
4pub mod shared_context;
5pub mod transport;
6
7pub const AUTOCHAT_MAX_AGENTS: usize = 8;
8pub const AUTOCHAT_DEFAULT_AGENTS: usize = 3;
9pub const AUTOCHAT_MAX_ROUNDS: usize = 3;
10pub const AUTOCHAT_MAX_DYNAMIC_SPAWNS: usize = 3;
11pub const AUTOCHAT_SPAWN_CHECK_MIN_CHARS: usize = 800;
12pub const AUTOCHAT_RLM_THRESHOLD_CHARS: usize = 6_000;
13pub const AUTOCHAT_RLM_FALLBACK_CHARS: usize = 3_500;
14pub const AUTOCHAT_STATUS_MAX_ROUNDS_REACHED: &str = "max_rounds_reached";
15pub const AUTOCHAT_NO_PRD_FLAG: &str = "--no-prd";
16pub const AUTOCHAT_RLM_HANDOFF_QUERY: &str = "Prepare a concise relay handoff for the next specialist.\n\
17Return FINAL(JSON) with this exact shape:\n\
18{\"kind\":\"semantic\",\"file\":\"relay_handoff\",\"answer\":\"...\"}\n\
19The \"answer\" must include:\n\
201) key conclusions,\n\
212) unresolved risks,\n\
223) one exact next action.\n\
23Keep it concise and actionable.";
24pub const AUTOCHAT_QUICK_DEMO_TASK: &str = "Self-organize into the right specialties for this task, then relay one concrete implementation plan with clear next handoffs.";
25
26const REQUIRED_RELAY_CAPABILITIES: [&str; 4] =
27    ["relay", "context-handoff", "rlm-aware", "autochat"];
28
29pub fn ensure_required_relay_capabilities(capabilities: &mut Vec<String>) {
30    for required in REQUIRED_RELAY_CAPABILITIES {
31        if !capabilities.iter().any(|cap| cap == required) {
32            capabilities.push(required.to_string());
33        }
34    }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ParsedAutochatRequest {
39    pub agent_count: usize,
40    pub task: String,
41    pub bypass_prd: bool,
42    pub explicit_count: bool,
43}
44
45pub fn parse_autochat_request(
46    rest: &str,
47    default_agents: usize,
48    quick_demo_task: &str,
49) -> Option<ParsedAutochatRequest> {
50    let mut bypass_prd = false;
51    let mut kept_tokens: Vec<&str> = Vec::new();
52
53    for token in rest.split_whitespace() {
54        if token.eq_ignore_ascii_case(AUTOCHAT_NO_PRD_FLAG) {
55            bypass_prd = true;
56        } else {
57            kept_tokens.push(token);
58        }
59    }
60
61    let normalized = kept_tokens.join(" ");
62    let explicit_count = normalized
63        .split_whitespace()
64        .next()
65        .and_then(|value| value.parse::<usize>().ok())
66        .is_some();
67    let (agent_count, task) = parse_autochat_args(&normalized, default_agents, quick_demo_task)?;
68
69    Some(ParsedAutochatRequest {
70        agent_count,
71        task: task.to_string(),
72        bypass_prd,
73        explicit_count,
74    })
75}
76
77pub fn parse_autochat_args<'a>(
78    rest: &'a str,
79    default_agents: usize,
80    quick_demo_task: &'a str,
81) -> Option<(usize, &'a str)> {
82    let rest = rest.trim();
83    if rest.is_empty() {
84        return None;
85    }
86
87    let mut parts = rest.splitn(2, char::is_whitespace);
88    let first = parts.next().unwrap_or("").trim();
89    if first.is_empty() {
90        return None;
91    }
92
93    if let Ok(count) = first.parse::<usize>() {
94        let task = parts.next().unwrap_or("").trim();
95        if task.is_empty() {
96            Some((count, quick_demo_task))
97        } else {
98            Some((count, task))
99        }
100    } else {
101        Some((default_agents, rest))
102    }
103}
104
105pub fn normalize_for_convergence(text: &str, max_chars: usize) -> String {
106    let mut normalized = String::with_capacity(text.len().min(512));
107    let mut last_was_space = false;
108
109    for ch in text.chars() {
110        if ch.is_ascii_alphanumeric() {
111            normalized.push(ch.to_ascii_lowercase());
112            last_was_space = false;
113        } else if ch.is_whitespace() && !last_was_space {
114            normalized.push(' ');
115            last_was_space = true;
116        }
117
118        if normalized.len() >= max_chars {
119            break;
120        }
121    }
122
123    normalized.trim().to_string()
124}
125
126#[cfg(test)]
127mod tests {
128    use super::{
129        AUTOCHAT_DEFAULT_AGENTS, AUTOCHAT_NO_PRD_FLAG, AUTOCHAT_QUICK_DEMO_TASK,
130        ParsedAutochatRequest, ensure_required_relay_capabilities, normalize_for_convergence,
131        parse_autochat_args, parse_autochat_request,
132    };
133
134    #[test]
135    fn parse_autochat_args_with_default_count() {
136        let parsed = parse_autochat_args(
137            "implement relay checkpointing",
138            AUTOCHAT_DEFAULT_AGENTS,
139            AUTOCHAT_QUICK_DEMO_TASK,
140        );
141        assert_eq!(
142            parsed,
143            Some((AUTOCHAT_DEFAULT_AGENTS, "implement relay checkpointing"))
144        );
145    }
146
147    #[test]
148    fn parse_autochat_args_with_count_only_uses_demo_task() {
149        let parsed = parse_autochat_args("4", AUTOCHAT_DEFAULT_AGENTS, AUTOCHAT_QUICK_DEMO_TASK);
150        assert_eq!(parsed, Some((4, AUTOCHAT_QUICK_DEMO_TASK)));
151    }
152
153    #[test]
154    fn normalize_for_convergence_ignores_case_and_symbols() {
155        let a = normalize_for_convergence("Done! Next Step: Add tests.", 280);
156        let b = normalize_for_convergence("done next step add tests", 280);
157        assert_eq!(a, b);
158    }
159
160    #[test]
161    fn ensure_required_relay_capabilities_adds_missing_caps() {
162        let mut caps = vec!["planning".to_string(), "relay".to_string()];
163        ensure_required_relay_capabilities(&mut caps);
164        assert!(caps.iter().any(|cap| cap == "context-handoff"));
165        assert!(caps.iter().any(|cap| cap == "rlm-aware"));
166        assert!(caps.iter().any(|cap| cap == "autochat"));
167    }
168
169    #[test]
170    fn parse_autochat_request_detects_no_prd_flag() {
171        let parsed = parse_autochat_request(
172            &format!("{AUTOCHAT_NO_PRD_FLAG} 4 build relay"),
173            AUTOCHAT_DEFAULT_AGENTS,
174            AUTOCHAT_QUICK_DEMO_TASK,
175        );
176        assert_eq!(
177            parsed,
178            Some(ParsedAutochatRequest {
179                agent_count: 4,
180                task: "build relay".to_string(),
181                bypass_prd: true,
182                explicit_count: true,
183            })
184        );
185    }
186
187    #[test]
188    fn parse_autochat_request_supports_flag_anywhere() {
189        let parsed = parse_autochat_request(
190            "build relay --no-prd now",
191            AUTOCHAT_DEFAULT_AGENTS,
192            AUTOCHAT_QUICK_DEMO_TASK,
193        );
194        assert_eq!(
195            parsed,
196            Some(ParsedAutochatRequest {
197                agent_count: AUTOCHAT_DEFAULT_AGENTS,
198                task: "build relay now".to_string(),
199                bypass_prd: true,
200                explicit_count: false,
201            })
202        );
203    }
204}