codetether_agent/session/helper/
build.rs1use super::text::latest_user_text;
2use crate::provider::{Message, ToolDefinition};
3use std::path::Path;
4
5pub fn looks_like_build_execution_request(text: &str) -> bool {
6 let lower = text.to_ascii_lowercase();
7 let keywords = [
8 "fix",
9 "patch",
10 "implement",
11 "add",
12 "update",
13 "edit",
14 "change",
15 "refactor",
16 "debug",
17 "investigate",
18 "run",
19 "test",
20 "build",
21 "compile",
22 "create",
23 "remove",
24 "rename",
25 "wire",
26 "hook up",
27 ];
28 keywords.iter().any(|k| lower.contains(k))
29}
30
31pub fn is_affirmative_build_followup(text: &str) -> bool {
32 let lower = text.trim().to_ascii_lowercase();
33 let markers = [
34 "yes",
35 "yep",
36 "yeah",
37 "do it",
38 "go ahead",
39 "proceed",
40 "use the edit",
41 "use edit",
42 "apply it",
43 "ship it",
44 "fix it",
45 ];
46 markers
47 .iter()
48 .any(|m| lower == *m || lower.starts_with(&format!("{m} ")))
49}
50
51pub fn looks_like_proposed_change(text: &str) -> bool {
52 let lower = text.to_ascii_lowercase();
53 let markers = [
54 "use this exact block",
55 "now uses",
56 "apply",
57 "replace",
58 "patch",
59 "edit",
60 "change",
61 "update",
62 "fix",
63 ];
64 markers.iter().any(|m| lower.contains(m))
65}
66
67pub fn assistant_offered_next_step(text: &str) -> bool {
68 let lower = text.to_ascii_lowercase();
69 let offer_markers = [
70 "if you want",
71 "want me to",
72 "should i",
73 "next i can",
74 "i can also",
75 "i'm ready to",
76 "i am ready to",
77 ];
78 let action_markers = [
79 "patch",
80 "add",
81 "update",
82 "edit",
83 "change",
84 "fix",
85 "implement",
86 "style",
87 "tighten",
88 "apply",
89 "refactor",
90 ];
91 offer_markers.iter().any(|m| lower.contains(m))
92 && action_markers.iter().any(|m| lower.contains(m))
93}
94
95pub fn is_build_agent(agent_name: &str) -> bool {
96 agent_name.eq_ignore_ascii_case("build")
97}
98
99pub fn should_force_build_tool_first_retry(
100 agent_name: &str,
101 retry_count: u8,
102 tool_definitions: &[ToolDefinition],
103 session_messages: &[Message],
104 workspace_dir: &Path,
105 assistant_text: &str,
106 has_tool_calls: bool,
107 build_mode_tool_first_max_retries: u8,
108) -> bool {
109 if retry_count >= build_mode_tool_first_max_retries
110 || !is_build_agent(agent_name)
111 || tool_definitions.is_empty()
112 || has_tool_calls
113 {
114 return false;
115 }
116
117 if assistant_text.trim().is_empty() {
118 return false;
119 }
120
121 if !build_request_requires_tool(session_messages, workspace_dir) {
122 return false;
123 }
124
125 true
126}
127
128pub fn build_request_requires_tool(session_messages: &[Message], workspace_dir: &Path) -> bool {
129 let Some(text) = latest_user_text(session_messages) else {
130 return false;
131 };
132
133 if looks_like_build_execution_request(&text)
134 && !super::text::extract_candidate_file_paths(&text, workspace_dir, 1).is_empty()
135 {
136 return true;
137 }
138
139 if !is_affirmative_build_followup(&text) {
140 return false;
141 }
142
143 let mut skipped_latest_user = false;
148 for msg in session_messages.iter().rev() {
149 let msg_text = super::text::extract_text_content(&msg.content);
150 if msg_text.trim().is_empty() {
151 continue;
152 }
153
154 if matches!(msg.role, crate::provider::Role::User) && !skipped_latest_user {
155 skipped_latest_user = true;
156 continue;
157 }
158
159 if matches!(msg.role, crate::provider::Role::Assistant)
160 && assistant_offered_next_step(&msg_text)
161 {
162 return true;
163 }
164
165 if (looks_like_build_execution_request(&msg_text) || looks_like_proposed_change(&msg_text))
166 && !super::text::extract_candidate_file_paths(&msg_text, workspace_dir, 1).is_empty()
167 {
168 return true;
169 }
170 }
171
172 false
173}