adk_cli/
console.rs

1use adk_core::{Agent, Content, Part};
2use adk_runner::{Runner, RunnerConfig};
3use adk_session::{CreateRequest, InMemorySessionService, SessionService};
4use anyhow::Result;
5use futures::StreamExt;
6use rustyline::DefaultEditor;
7use serde_json::Value;
8use std::collections::HashMap;
9use std::io::{self, Write};
10use std::sync::Arc;
11
12#[allow(dead_code)] // Part of CLI API, not currently used
13pub async fn run_console(agent: Arc<dyn Agent>, app_name: String, user_id: String) -> Result<()> {
14    let session_service = Arc::new(InMemorySessionService::new());
15
16    let session = session_service
17        .create(CreateRequest {
18            app_name: app_name.clone(),
19            user_id: user_id.clone(),
20            session_id: None,
21            state: HashMap::new(),
22        })
23        .await?;
24
25    let runner = Runner::new(RunnerConfig {
26        app_name: app_name.clone(),
27        agent: agent.clone(),
28        session_service: session_service.clone(),
29        artifact_service: None,
30        memory_service: None,
31        run_config: None,
32    })?;
33
34    let mut rl = DefaultEditor::new()?;
35
36    println!("ADK Console Mode");
37    println!("Agent: {}", agent.name());
38    println!("Type your message and press Enter. Ctrl+C to exit.\n");
39
40    loop {
41        let readline = rl.readline("User -> ");
42        match readline {
43            Ok(line) => {
44                if line.trim().is_empty() {
45                    continue;
46                }
47
48                rl.add_history_entry(&line)?;
49
50                let user_content = Content::new("user").with_text(line);
51
52                print!("\nAgent -> ");
53
54                let session_id = session.id().to_string();
55                let mut events = runner.run(user_id.clone(), session_id, user_content).await?;
56
57                let mut stream_printer = StreamPrinter::default();
58
59                while let Some(event) = events.next().await {
60                    match event {
61                        Ok(evt) => {
62                            if let Some(content) = &evt.llm_response.content {
63                                for part in &content.parts {
64                                    stream_printer.handle_part(part);
65                                }
66                            }
67                        }
68                        Err(e) => {
69                            eprintln!("\nError: {}", e);
70                        }
71                    }
72                }
73
74                stream_printer.finish();
75                println!("\n");
76            }
77            Err(rustyline::error::ReadlineError::Interrupted) => {
78                println!("Interrupted");
79                break;
80            }
81            Err(rustyline::error::ReadlineError::Eof) => {
82                println!("EOF");
83                break;
84            }
85            Err(err) => {
86                eprintln!("Error: {}", err);
87                break;
88            }
89        }
90    }
91
92    Ok(())
93}
94
95/// StreamPrinter handles streaming output with special handling for:
96/// - `<think>` blocks: Displayed as `[think] ...` for reasoning models
97/// - Tool calls and responses: Formatted output
98/// - Regular text: Streamed directly to stdout
99#[derive(Default)]
100struct StreamPrinter {
101    in_think_block: bool,
102    think_buffer: String,
103}
104
105impl StreamPrinter {
106    fn handle_part(&mut self, part: &Part) {
107        match part {
108            Part::Text { text } => self.handle_text_chunk(text),
109            Part::FunctionCall { name, args, .. } => self.print_tool_call(name, args),
110            Part::FunctionResponse { function_response, .. } => {
111                self.print_tool_response(&function_response.name, &function_response.response)
112            }
113            Part::InlineData { mime_type, data } => self.print_inline_data(mime_type, data.len()),
114            Part::FileData { mime_type, file_uri } => self.print_file_data(mime_type, file_uri),
115        }
116    }
117
118    fn handle_text_chunk(&mut self, chunk: &str) {
119        const THINK_START: &str = "<think>";
120        const THINK_END: &str = "</think>";
121
122        let mut remaining = chunk;
123
124        while !remaining.is_empty() {
125            if self.in_think_block {
126                if let Some(end_idx) = remaining.find(THINK_END) {
127                    self.think_buffer.push_str(&remaining[..end_idx]);
128                    self.flush_think();
129                    self.in_think_block = false;
130                    remaining = &remaining[end_idx + THINK_END.len()..];
131                } else {
132                    self.think_buffer.push_str(remaining);
133                    break;
134                }
135            } else if let Some(start_idx) = remaining.find(THINK_START) {
136                let visible = &remaining[..start_idx];
137                self.print_visible(visible);
138                self.in_think_block = true;
139                self.think_buffer.clear();
140                remaining = &remaining[start_idx + THINK_START.len()..];
141            } else {
142                self.print_visible(remaining);
143                break;
144            }
145        }
146    }
147
148    fn print_visible(&self, text: &str) {
149        if text.is_empty() {
150            return;
151        }
152
153        print!("{}", text);
154        let _ = io::stdout().flush();
155    }
156
157    fn flush_think(&mut self) {
158        let content = self.think_buffer.trim();
159        if content.is_empty() {
160            self.think_buffer.clear();
161            return;
162        }
163
164        print!("\n[think] {}\n", content);
165        let _ = io::stdout().flush();
166        self.think_buffer.clear();
167    }
168
169    fn finish(&mut self) {
170        if self.in_think_block {
171            self.flush_think();
172            self.in_think_block = false;
173        }
174    }
175
176    fn print_tool_call(&self, name: &str, args: &Value) {
177        print!("\n[tool-call] {} {}\n", name, args);
178        let _ = io::stdout().flush();
179    }
180
181    fn print_tool_response(&self, name: &str, response: &Value) {
182        print!("\n[tool-response] {} {}\n", name, response);
183        let _ = io::stdout().flush();
184    }
185
186    fn print_inline_data(&self, mime_type: &str, len: usize) {
187        print!("\n[inline-data] mime={} bytes={}\n", mime_type, len);
188        let _ = io::stdout().flush();
189    }
190
191    fn print_file_data(&self, mime_type: &str, file_uri: &str) {
192        print!("\n[file-data] mime={} uri={}\n", mime_type, file_uri);
193        let _ = io::stdout().flush();
194    }
195}