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        }
114    }
115
116    fn handle_text_chunk(&mut self, chunk: &str) {
117        const THINK_START: &str = "<think>";
118        const THINK_END: &str = "</think>";
119
120        let mut remaining = chunk;
121
122        while !remaining.is_empty() {
123            if self.in_think_block {
124                if let Some(end_idx) = remaining.find(THINK_END) {
125                    self.think_buffer.push_str(&remaining[..end_idx]);
126                    self.flush_think();
127                    self.in_think_block = false;
128                    remaining = &remaining[end_idx + THINK_END.len()..];
129                } else {
130                    self.think_buffer.push_str(remaining);
131                    break;
132                }
133            } else if let Some(start_idx) = remaining.find(THINK_START) {
134                let visible = &remaining[..start_idx];
135                self.print_visible(visible);
136                self.in_think_block = true;
137                self.think_buffer.clear();
138                remaining = &remaining[start_idx + THINK_START.len()..];
139            } else {
140                self.print_visible(remaining);
141                break;
142            }
143        }
144    }
145
146    fn print_visible(&self, text: &str) {
147        if text.is_empty() {
148            return;
149        }
150
151        print!("{}", text);
152        let _ = io::stdout().flush();
153    }
154
155    fn flush_think(&mut self) {
156        let content = self.think_buffer.trim();
157        if content.is_empty() {
158            self.think_buffer.clear();
159            return;
160        }
161
162        print!("\n[think] {}\n", content);
163        let _ = io::stdout().flush();
164        self.think_buffer.clear();
165    }
166
167    fn finish(&mut self) {
168        if self.in_think_block {
169            self.flush_think();
170            self.in_think_block = false;
171        }
172    }
173
174    fn print_tool_call(&self, name: &str, args: &Value) {
175        print!("\n[tool-call] {} {}\n", name, args);
176        let _ = io::stdout().flush();
177    }
178
179    fn print_tool_response(&self, name: &str, response: &Value) {
180        print!("\n[tool-response] {} {}\n", name, response);
181        let _ = io::stdout().flush();
182    }
183
184    fn print_inline_data(&self, mime_type: &str, len: usize) {
185        print!("\n[inline-data] mime={} bytes={}\n", mime_type, len);
186        let _ = io::stdout().flush();
187    }
188}