toast_api/
agent_cli.rs

1//! Agent-based CLI implementation
2
3use crate::agent::{AgentConfig, AgentSession};
4use crate::api::{Claude, Session as ClaudeSession};
5use crate::deepseek::{DeepSeek, Session as DeepSeekSession};
6use crate::utils::prettify;
7use anyhow::{anyhow, Context, Result};
8use std::io::{self, Write};
9
10/// Run the agent-based CLI
11pub async fn run_agent_cli(
12    use_deepseek: bool,
13    use_opus: bool,
14    use_haiku: bool,
15) -> Result<()> {
16    println!("šŸ¤– Starting Toast Agent...\n");
17
18    // Create agent with default config
19    let config = AgentConfig::default();
20    let mut session = AgentSession::new(config);
21
22    if use_deepseek {
23        run_with_deepseek(&mut session, use_opus, use_haiku).await
24    } else {
25        run_with_claude(&mut session, use_opus, use_haiku).await
26    }
27}
28
29async fn run_with_claude(
30    session: &mut AgentSession,
31    use_opus: bool,
32    use_haiku: bool,
33) -> Result<()> {
34    // Load Claude configuration
35    let config_dir = dirs::config_dir()
36        .ok_or_else(|| anyhow!("Could not determine config directory"))?
37        .join("toast");
38
39    let cookie = std::fs::read_to_string(config_dir.join("cookie"))
40        .context("Failed to read cookie")?
41        .trim()
42        .to_string();
43
44    let org_id = if let Ok(id) = std::fs::read_to_string(config_dir.join("org_id")) {
45        id.trim().to_string()
46    } else {
47        crate::utils::extract_org_id_from_cookie(&cookie)
48            .ok_or_else(|| anyhow!("Could not extract org_id from cookie"))?
49    };
50
51    let claude_session = ClaudeSession {
52        cookie,
53        user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0".to_string(),
54        organization_id: org_id,
55    };
56
57    let model = if use_opus {
58        crate::config::OPUS_MODEL
59    } else if use_haiku {
60        crate::config::HAIKU_MODEL
61    } else {
62        crate::config::SONNET_MODEL
63    };
64
65    let claude = Claude::new(claude_session, model)?;
66    println!("Connected to Claude ({model})\n");
67
68    // Create chat
69    let chat_id = claude.create_chat().await.context("Failed to create chat")?;
70    
71    // Send system prompt with tool descriptions
72    let system_prompt = session.agent().get_system_prompt();
73    claude.send_message(&chat_id, &system_prompt, &[]).await
74        .context("Failed to send system prompt")?;
75
76    // Main interaction loop
77    let stdin = io::stdin();
78    let mut stdout = io::stdout();
79
80    loop {
81        print!("You: ");
82        stdout.flush()?;
83
84        let mut input = String::new();
85        match stdin.read_line(&mut input) {
86            Ok(0) => break, // EOF
87            Ok(_) => {
88                let input = input.trim();
89                if input.is_empty() {
90                    continue;
91                }
92                if matches!(input, "exit" | "quit" | "/exit" | "x") {
93                    break;
94                }
95
96                // Send message to Claude
97                let response = claude.send_message(&chat_id, input, &[]).await
98                    .context("Failed to send message")?;
99
100                println!("\nClaude: {}\n", prettify(&response));
101
102                // Process any tool calls in the response
103                process_agent_response_claude(session, &claude, &chat_id, &response).await?;
104                
105                // Reset iteration count for next user interaction
106                session.agent().reset();
107            }
108            Err(e) => {
109                eprintln!("Error reading input: {e}");
110                break;
111            }
112        }
113    }
114
115    // Clean up
116    claude.delete_chat(&chat_id).await.ok();
117    println!("\nšŸ‘‹ Goodbye!");
118    Ok(())
119}
120
121async fn run_with_deepseek(
122    session: &mut AgentSession,
123    use_opus: bool,
124    use_haiku: bool,
125) -> Result<()> {
126    // Load DeepSeek configuration
127    let config_dir = dirs::config_dir()
128        .ok_or_else(|| anyhow!("Could not determine config directory"))?
129        .join("toast")
130        .join("deepseek");
131
132    let auth_token = std::fs::read_to_string(config_dir.join("auth_token"))
133        .context("Failed to read auth token")?
134        .trim()
135        .to_string();
136
137    let cookies = serde_json::from_str(
138        &std::fs::read_to_string(config_dir.join("cookies.json"))
139            .context("Failed to read cookies")?
140    )?;
141
142    let deepseek_session = DeepSeekSession { auth_token, cookies };
143    let mut deepseek = DeepSeek::new(deepseek_session)?;
144
145    let model = if use_opus {
146        "deepseek-r1"
147    } else if use_haiku {
148        "deepseek-lite"
149    } else {
150        "deepseek-r1"
151    };
152
153    println!("Connected to DeepSeek ({model})\n");
154
155    // Create chat session
156    let chat_id = deepseek.create_chat_session().await
157        .context("Failed to create chat session")?;
158
159    let thinking_mode = if model == "deepseek-r1" {
160        crate::deepseek::ThinkingMode::Detailed
161    } else {
162        crate::deepseek::ThinkingMode::Simple
163    };
164    let search_mode = crate::deepseek::SearchMode::Disabled;
165
166    // Send initial system prompt
167    let system_prompt = session.agent().get_system_prompt();
168    
169    // Main interaction loop
170    let stdin = io::stdin();
171    let mut stdout = io::stdout();
172    let mut first_message = true;
173
174    loop {
175        print!("You: ");
176        stdout.flush()?;
177
178        let mut input = String::new();
179        match stdin.read_line(&mut input) {
180            Ok(0) => break, // EOF
181            Ok(_) => {
182                let input = input.trim();
183                if input.is_empty() {
184                    continue;
185                }
186                if matches!(input, "exit" | "quit" | "/exit" | "x") {
187                    break;
188                }
189
190                // Include system prompt only on first message
191                let system_prompt_opt = if first_message {
192                    first_message = false;
193                    Some(system_prompt.as_str())
194                } else {
195                    None
196                };
197
198                // Send message to DeepSeek
199                let response = deepseek.chat_completion(
200                    &chat_id,
201                    input,
202                    None,
203                    thinking_mode,
204                    search_mode,
205                    system_prompt_opt,
206                ).await.context("Failed to send message")?;
207
208                println!("\nDeepSeek: {}\n", prettify(&response));
209
210                // Process any tool calls in the response
211                process_agent_response_deepseek(
212                    session,
213                    &mut deepseek,
214                    &chat_id,
215                    &response,
216                    thinking_mode,
217                    search_mode,
218                ).await?;
219                
220                // Reset iteration count for next user interaction
221                session.agent().reset();
222            }
223            Err(e) => {
224                eprintln!("Error reading input: {e}");
225                break;
226            }
227        }
228    }
229
230    println!("\nšŸ‘‹ Goodbye!");
231    Ok(())
232}
233
234async fn process_agent_response_claude(
235    session: &mut AgentSession,
236    claude: &Claude,
237    chat_id: &str,
238    response: &str,
239) -> Result<()> {
240    let tool_results = session.agent().process_tool_calls(response).await?;
241    
242    if !tool_results.is_empty() {
243        // Format tool results for Claude
244        let mut result_message = String::from("Tool execution results:\n\n");
245        for (tool_name, output) in tool_results {
246            result_message.push_str(&format!("[{tool_name}]\n{output}\n\n"));
247        }
248
249        // Send results back to Claude
250        let follow_up = claude.send_message(chat_id, &result_message, &[]).await
251            .context("Failed to send tool results")?;
252
253        println!("Claude: {}\n", prettify(&follow_up));
254
255        // Recursively process any new tool calls
256        Box::pin(process_agent_response_claude(session, claude, chat_id, &follow_up)).await?;
257    }
258
259    Ok(())
260}
261
262async fn process_agent_response_deepseek(
263    session: &mut AgentSession,
264    deepseek: &mut DeepSeek,
265    chat_id: &str,
266    response: &str,
267    thinking_mode: crate::deepseek::ThinkingMode,
268    search_mode: crate::deepseek::SearchMode,
269) -> Result<()> {
270    let tool_results = session.agent().process_tool_calls(response).await?;
271    
272    if !tool_results.is_empty() {
273        // Format tool results for DeepSeek
274        let mut result_message = String::from("Tool execution results:\n\n");
275        for (tool_name, output) in tool_results {
276            result_message.push_str(&format!("[{tool_name}]\n{output}\n\n"));
277        }
278
279        // Send results back to DeepSeek
280        let follow_up = deepseek.chat_completion(
281            chat_id,
282            &result_message,
283            None,
284            thinking_mode,
285            search_mode,
286            None,
287        ).await.context("Failed to send tool results")?;
288
289        println!("DeepSeek: {}\n", prettify(&follow_up));
290
291        // Recursively process any new tool calls
292        Box::pin(process_agent_response_deepseek(
293            session,
294            deepseek,
295            chat_id,
296            &follow_up,
297            thinking_mode,
298            search_mode,
299        )).await?;
300    }
301
302    Ok(())
303}