codetether_agent/autochat/
mod.rs1pub 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}