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