Skip to main content

skilllite_agent/types/
event_sink.rs

1//! Event sink trait and implementations for different output targets.
2
3use super::string_utils::safe_truncate;
4use super::task::Task;
5
6/// Structured request asking the user for clarification before the agent stops.
7#[derive(Debug, Clone, serde::Serialize)]
8pub struct ClarificationRequest {
9    pub reason: String,
10    pub message: String,
11    pub suggestions: Vec<String>,
12}
13
14/// User's response to a clarification request.
15#[derive(Debug, Clone)]
16pub enum ClarificationResponse {
17    /// Continue execution; optional hint injected as a user message.
18    Continue(Option<String>),
19    /// Stop the agent loop.
20    Stop,
21}
22
23/// Event sink trait for different output targets (CLI, RPC, SDK).
24pub trait EventSink: Send {
25    /// Called at the start of each conversation turn (before any other events).
26    fn on_turn_start(&mut self) {}
27    /// Called when the assistant produces text content.
28    fn on_text(&mut self, text: &str);
29    /// Called when a tool is about to be invoked.
30    fn on_tool_call(&mut self, name: &str, arguments: &str);
31    /// Called when a tool returns a result.
32    fn on_tool_result(&mut self, name: &str, result: &str, is_error: bool);
33    /// Called when a command tool starts execution.
34    fn on_command_started(&mut self, _command: &str) {}
35    /// Called when a command tool emits incremental stdout/stderr output.
36    fn on_command_output(&mut self, _stream: &str, _chunk: &str) {}
37    /// Called when a command tool finishes execution.
38    fn on_command_finished(&mut self, _success: bool, _exit_code: i32, _duration_ms: u64) {}
39    /// Called when preview server startup begins.
40    fn on_preview_started(&mut self, _path: &str, _port: u16) {}
41    /// Called when preview server is ready.
42    fn on_preview_ready(&mut self, _url: &str, _port: u16) {}
43    /// Called when preview server startup fails.
44    fn on_preview_failed(&mut self, _message: &str) {}
45    /// Called when preview server stops.
46    fn on_preview_stopped(&mut self, _reason: &str) {}
47    /// Called when swarm delegation starts.
48    fn on_swarm_started(&mut self, _description: &str) {}
49    /// Called with lightweight swarm progress updates.
50    fn on_swarm_progress(&mut self, _status: &str) {}
51    /// Called when swarm delegation finishes with a summary.
52    fn on_swarm_finished(&mut self, _summary: &str) {}
53    /// Called when swarm delegation fails or falls back.
54    fn on_swarm_failed(&mut self, _message: &str) {}
55    /// Called when the agent needs user confirmation (L3 security).
56    /// Returns true if the user approves.
57    fn on_confirmation_request(&mut self, prompt: &str) -> bool;
58    /// Called for streaming text chunks.
59    fn on_text_chunk(&mut self, _chunk: &str) {}
60    /// Called when a task plan is generated. (Phase 2)
61    fn on_task_plan(&mut self, _tasks: &[Task]) {}
62    /// Called when a task's status changes. (Phase 2)
63    /// `tasks` contains the full updated task list for progress rendering.
64    fn on_task_progress(&mut self, _task_id: u32, _completed: bool, _tasks: &[Task]) {}
65    /// Called when the agent is about to stop and wants user clarification.
66    /// Returns `Continue(hint)` to keep going or `Stop` to terminate.
67    fn on_clarification_request(
68        &mut self,
69        _request: &ClarificationRequest,
70    ) -> ClarificationResponse {
71        ClarificationResponse::Stop
72    }
73}
74
75/// Silent event sink for background operations (e.g. pre-compaction memory flush).
76/// Swallows all output and auto-approves confirmation requests.
77pub struct SilentEventSink;
78
79impl EventSink for SilentEventSink {
80    fn on_text(&mut self, _text: &str) {}
81    fn on_tool_call(&mut self, _name: &str, _arguments: &str) {}
82    fn on_tool_result(&mut self, _name: &str, _result: &str, _is_error: bool) {}
83    fn on_confirmation_request(&mut self, _prompt: &str) -> bool {
84        true // Auto-approve for silent operations (memory flush may rarely need run_command)
85    }
86}
87
88/// Separator for CLI section headers.
89const SECTION_SEP: &str = "──────────────────────────────────────";
90
91/// Simple terminal event sink for CLI chat.
92pub struct TerminalEventSink {
93    pub verbose: bool,
94    streamed_text: bool,
95    /// Whether we've shown the "执行" section header this turn.
96    execution_section_shown: bool,
97    /// Whether we've shown the "结果" section header this turn.
98    result_section_shown: bool,
99}
100
101impl TerminalEventSink {
102    pub fn new(verbose: bool) -> Self {
103        Self {
104            verbose,
105            streamed_text: false,
106            execution_section_shown: false,
107            result_section_shown: false,
108        }
109    }
110
111    #[inline]
112    fn msg(&self, s: &str) {
113        eprintln!("{}", s);
114    }
115
116    #[inline]
117    fn msg_opt(&self, s: &str) {
118        if !s.is_empty() {
119            for line in s.lines() {
120                eprintln!("{}", line);
121            }
122        }
123    }
124
125    fn show_execution_section(&mut self) {
126        if !self.execution_section_shown {
127            self.execution_section_shown = true;
128            self.msg(&format!("─── 🔧 执行 ─── {}", SECTION_SEP));
129        }
130    }
131
132    fn show_result_section(&mut self) {
133        if !self.result_section_shown {
134            self.result_section_shown = true;
135            self.msg(&format!("─── 📄 结果 ─── {}", SECTION_SEP));
136            self.msg("");
137        }
138    }
139}
140
141impl EventSink for TerminalEventSink {
142    fn on_turn_start(&mut self) {
143        self.execution_section_shown = false;
144        self.result_section_shown = false;
145    }
146
147    fn on_text(&mut self, text: &str) {
148        if self.streamed_text {
149            // Text was already displayed chunk-by-chunk via on_text_chunk.
150            // The trailing newline was also added by accumulate_stream.
151            // Just reset the flag for the next response.
152            self.streamed_text = false;
153            return;
154        }
155        // Non-streaming path: display full text + newline
156        // Only show result section when we have actual content (avoids empty "结果" between plan and execution)
157        if !text.trim().is_empty() {
158            self.show_result_section();
159        }
160        use std::io::Write;
161        print!("{}", text);
162        let _ = std::io::stdout().flush();
163        println!();
164    }
165
166    fn on_text_chunk(&mut self, chunk: &str) {
167        self.streamed_text = true;
168        // Only show result section when we have actual content (avoids empty "结果" between plan and execution)
169        if !chunk.trim().is_empty() {
170            self.show_result_section();
171        }
172        use std::io::Write;
173        print!("{}", chunk);
174        let _ = std::io::stdout().flush();
175    }
176
177    fn on_tool_call(&mut self, name: &str, arguments: &str) {
178        self.show_execution_section();
179        if self.verbose {
180            // Truncate long JSON args for display
181            let args_display = if arguments.len() > 200 {
182                format!("{}…", safe_truncate(arguments, 200))
183            } else {
184                arguments.to_string()
185            };
186            self.msg(&format!("🔧 Tool: {}  args={}", name, args_display));
187        } else {
188            self.msg(&format!("🔧 {}", name));
189        }
190    }
191
192    fn on_tool_result(&mut self, name: &str, result: &str, is_error: bool) {
193        let icon = if is_error { "❌" } else { "✅" };
194        if self.verbose {
195            let brief = if result.len() > 400 {
196                format!("{}…", safe_truncate(result, 400))
197            } else {
198                result.to_string()
199            };
200            self.msg(&format!("  {} {}: {}", icon, name, brief));
201        } else {
202            let first = result.lines().next().unwrap_or("(ok)");
203            let brief = if first.len() > 80 {
204                format!("{}…", safe_truncate(first, 80))
205            } else {
206                first.to_string()
207            };
208            self.msg(&format!("  {} {} {}", icon, name, brief));
209        }
210    }
211
212    fn on_command_started(&mut self, command: &str) {
213        self.show_execution_section();
214        let brief = if command.len() > 120 {
215            format!("{}…", safe_truncate(command, 120))
216        } else {
217            command.to_string()
218        };
219        self.msg(&format!("  ▶ command started: {}", brief));
220    }
221
222    fn on_command_output(&mut self, stream: &str, chunk: &str) {
223        if chunk.is_empty() {
224            return;
225        }
226        self.show_execution_section();
227        let prefix = if stream == "stderr" { "  ! " } else { "  │ " };
228        for line in chunk.lines() {
229            self.msg(&format!("{}{}", prefix, line));
230        }
231    }
232
233    fn on_command_finished(&mut self, success: bool, exit_code: i32, duration_ms: u64) {
234        self.show_execution_section();
235        let icon = if success { "  ■" } else { "  ✗" };
236        self.msg(&format!(
237            "{} command finished: exit {} ({} ms)",
238            icon, exit_code, duration_ms
239        ));
240    }
241
242    fn on_preview_started(&mut self, path: &str, port: u16) {
243        self.show_execution_section();
244        self.msg(&format!("  ▶ preview started: {} (port {})", path, port));
245    }
246
247    fn on_preview_ready(&mut self, url: &str, _port: u16) {
248        self.show_execution_section();
249        self.msg(&format!("  ■ preview ready: {}", url));
250    }
251
252    fn on_preview_failed(&mut self, message: &str) {
253        self.show_execution_section();
254        self.msg(&format!("  ✗ preview failed: {}", message));
255    }
256
257    fn on_preview_stopped(&mut self, reason: &str) {
258        self.show_execution_section();
259        self.msg(&format!("  ■ preview stopped: {}", reason));
260    }
261
262    fn on_swarm_started(&mut self, description: &str) {
263        self.show_execution_section();
264        let brief = if description.len() > 120 {
265            format!("{}…", safe_truncate(description, 120))
266        } else {
267            description.to_string()
268        };
269        self.msg(&format!("  ▶ swarm started: {}", brief));
270    }
271
272    fn on_swarm_progress(&mut self, status: &str) {
273        self.show_execution_section();
274        self.msg(&format!("  … swarm: {}", status));
275    }
276
277    fn on_swarm_finished(&mut self, summary: &str) {
278        self.show_execution_section();
279        let brief = if summary.len() > 160 {
280            format!("{}…", safe_truncate(summary, 160))
281        } else {
282            summary.to_string()
283        };
284        self.msg(&format!("  ■ swarm finished: {}", brief));
285    }
286
287    fn on_swarm_failed(&mut self, message: &str) {
288        self.show_execution_section();
289        let brief = if message.len() > 160 {
290            format!("{}…", safe_truncate(message, 160))
291        } else {
292            message.to_string()
293        };
294        self.msg(&format!("  ✗ swarm failed: {}", brief));
295    }
296
297    fn on_confirmation_request(&mut self, prompt: &str) -> bool {
298        use std::io::Write;
299        self.msg_opt(prompt);
300        eprint!("确认执行? [y/N] ");
301        let _ = std::io::stderr().flush();
302        let mut input = String::new();
303        if std::io::stdin().read_line(&mut input).is_ok() {
304            let trimmed = input.trim().to_lowercase();
305            trimmed == "y" || trimmed == "yes"
306        } else {
307            false
308        }
309    }
310
311    fn on_clarification_request(
312        &mut self,
313        request: &ClarificationRequest,
314    ) -> ClarificationResponse {
315        use std::io::Write;
316        self.msg(&format!("─── ⚠ 需要确认 ─── {}", SECTION_SEP));
317        self.msg(&format!("原因: {}", request.reason));
318        self.msg(&request.message);
319        self.msg("");
320        for (i, s) in request.suggestions.iter().enumerate() {
321            self.msg(&format!("  [{}] {}", i + 1, s));
322        }
323        self.msg("  [0] 停止");
324        eprint!("请选择 (或直接输入补充信息): ");
325        let _ = std::io::stderr().flush();
326        let mut input = String::new();
327        if std::io::stdin().read_line(&mut input).is_ok() {
328            let trimmed = input.trim();
329            if trimmed == "0" {
330                return ClarificationResponse::Stop;
331            }
332            if let Ok(idx) = trimmed.parse::<usize>() {
333                if idx >= 1 && idx <= request.suggestions.len() {
334                    return ClarificationResponse::Continue(Some(
335                        request.suggestions[idx - 1].clone(),
336                    ));
337                }
338            }
339            if !trimmed.is_empty() {
340                return ClarificationResponse::Continue(Some(trimmed.to_string()));
341            }
342        }
343        ClarificationResponse::Stop
344    }
345
346    fn on_task_plan(&mut self, tasks: &[Task]) {
347        self.msg(&format!("─── 📋 计划 ─── {}", SECTION_SEP));
348        self.msg(&format!("Task plan ({} tasks):", tasks.len()));
349        for task in tasks {
350            let status = if task.completed { "✅" } else { "○" };
351            let hint = task
352                .tool_hint
353                .as_deref()
354                .map(|h| format!(" [{}]", h))
355                .unwrap_or_default();
356            self.msg(&format!(
357                "   {}. {} {}{}",
358                task.id, status, task.description, hint
359            ));
360        }
361    }
362
363    fn on_task_progress(&mut self, task_id: u32, completed: bool, tasks: &[Task]) {
364        if completed {
365            self.msg(&format!("  ✅ Task {} completed", task_id));
366        }
367        if !tasks.is_empty() {
368            let completed_count = tasks.iter().filter(|t| t.completed).count();
369            self.msg(&format!("  📋 进度 ({}/{}):", completed_count, tasks.len()));
370            for task in tasks {
371                let status = if task.completed {
372                    "✅"
373                } else if task.id
374                    == tasks
375                        .iter()
376                        .find(|t| !t.completed)
377                        .map(|t| t.id)
378                        .unwrap_or(0)
379                {
380                    "▶"
381                } else {
382                    "○"
383                };
384                let hint = task
385                    .tool_hint
386                    .as_deref()
387                    .map(|h| format!(" [{}]", h))
388                    .unwrap_or_default();
389                self.msg(&format!(
390                    "     {}. {} {}{}",
391                    task.id, status, task.description, hint
392                ));
393            }
394        }
395    }
396}
397
398/// Event sink for unattended run mode: same output as TerminalEventSink,
399/// but auto-approves confirmation requests (run_command, L3 skill scan).
400/// Replan (update_task_plan) never waits — agent continues immediately.
401pub struct RunModeEventSink {
402    inner: TerminalEventSink,
403}
404
405impl RunModeEventSink {
406    pub fn new(verbose: bool) -> Self {
407        Self {
408            inner: TerminalEventSink::new(verbose),
409        }
410    }
411}
412
413impl EventSink for RunModeEventSink {
414    fn on_turn_start(&mut self) {
415        self.inner.on_turn_start();
416    }
417    fn on_text(&mut self, text: &str) {
418        self.inner.on_text(text);
419    }
420    fn on_text_chunk(&mut self, chunk: &str) {
421        self.inner.on_text_chunk(chunk);
422    }
423    fn on_tool_call(&mut self, name: &str, arguments: &str) {
424        self.inner.on_tool_call(name, arguments);
425    }
426    fn on_tool_result(&mut self, name: &str, result: &str, is_error: bool) {
427        self.inner.on_tool_result(name, result, is_error);
428    }
429    fn on_command_started(&mut self, command: &str) {
430        self.inner.on_command_started(command);
431    }
432    fn on_command_output(&mut self, stream: &str, chunk: &str) {
433        self.inner.on_command_output(stream, chunk);
434    }
435    fn on_command_finished(&mut self, success: bool, exit_code: i32, duration_ms: u64) {
436        self.inner
437            .on_command_finished(success, exit_code, duration_ms);
438    }
439    fn on_preview_started(&mut self, path: &str, port: u16) {
440        self.inner.on_preview_started(path, port);
441    }
442    fn on_preview_ready(&mut self, url: &str, port: u16) {
443        self.inner.on_preview_ready(url, port);
444    }
445    fn on_preview_failed(&mut self, message: &str) {
446        self.inner.on_preview_failed(message);
447    }
448    fn on_preview_stopped(&mut self, reason: &str) {
449        self.inner.on_preview_stopped(reason);
450    }
451    fn on_swarm_started(&mut self, description: &str) {
452        self.inner.on_swarm_started(description);
453    }
454    fn on_swarm_progress(&mut self, status: &str) {
455        self.inner.on_swarm_progress(status);
456    }
457    fn on_swarm_finished(&mut self, summary: &str) {
458        self.inner.on_swarm_finished(summary);
459    }
460    fn on_swarm_failed(&mut self, message: &str) {
461        self.inner.on_swarm_failed(message);
462    }
463    fn on_confirmation_request(&mut self, prompt: &str) -> bool {
464        if !prompt.is_empty() {
465            for line in prompt.lines() {
466                eprintln!("{}", line);
467            }
468        }
469        eprintln!("  [run mode: auto-approved]");
470        true
471    }
472    fn on_clarification_request(
473        &mut self,
474        request: &ClarificationRequest,
475    ) -> ClarificationResponse {
476        eprintln!(
477            "  [run mode: auto-stop on clarification] reason={} msg={}",
478            request.reason, request.message
479        );
480        ClarificationResponse::Stop
481    }
482    fn on_task_plan(&mut self, tasks: &[Task]) {
483        self.inner.on_task_plan(tasks);
484    }
485    fn on_task_progress(&mut self, task_id: u32, completed: bool, tasks: &[Task]) {
486        self.inner.on_task_progress(task_id, completed, tasks);
487    }
488}