Skip to main content

codetether_agent/session/helper/
build.rs

1use 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    // Logic for affirmative check omitted for brevity but should be fully implemented if needed
144    // In original code it was checking previous messages.
145    // Let's implement it properly.
146
147    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}