use crate::agent::{StopReason, Usage};
use crate::message::{Message, ToolCall};
use async_trait::async_trait;
use std::io::Write;
pub type ListenerError = Box<dyn std::error::Error + Send + Sync>;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum RunEvent<'a> {
TextDelta(&'a str),
ReasoningDelta(&'a str),
ToolCall(&'a ToolCall),
ToolResult {
call_id: &'a str,
tool: &'a str,
content: &'a str,
is_error: bool,
},
Finished {
new_messages: &'a [Message],
usage: &'a Usage,
stop_reason: Option<&'a StopReason>,
iterations: usize,
},
}
pub trait EventListener: Send {
fn on_event(&mut self, event: &RunEvent<'_>) -> Result<(), ListenerError>;
}
#[async_trait]
pub trait AsyncEventListener: Send {
async fn on_event(&mut self, event: &RunEvent<'_>) -> Result<(), ListenerError>;
}
impl<L: EventListener + ?Sized> EventListener for &mut L {
fn on_event(&mut self, event: &RunEvent<'_>) -> Result<(), ListenerError> {
(**self).on_event(event)
}
}
impl<L: EventListener + ?Sized> EventListener for Box<L> {
fn on_event(&mut self, event: &RunEvent<'_>) -> Result<(), ListenerError> {
(**self).on_event(event)
}
}
#[derive(Debug)]
pub struct Printer<W> {
out: W,
tool_calls: bool,
tool_results: bool,
reasoning: bool,
line_open: bool,
in_reasoning: bool,
}
impl<W> Printer<W> {
pub fn new(out: W) -> Self {
Self {
out,
tool_calls: true,
tool_results: false,
reasoning: false,
line_open: false,
in_reasoning: false,
}
}
pub fn with_tool_calls(mut self, tool_calls: bool) -> Self {
self.tool_calls = tool_calls;
self
}
pub fn with_tool_results(mut self, tool_results: bool) -> Self {
self.tool_results = tool_results;
self
}
pub fn with_reasoning(mut self, reasoning: bool) -> Self {
self.reasoning = reasoning;
self
}
pub fn into_inner(self) -> W {
self.out
}
fn render(&mut self, event: &RunEvent<'_>) -> String {
let mut out = String::new();
match *event {
RunEvent::TextDelta(text) => {
if self.in_reasoning {
self.end_block(&mut out);
}
self.push_text(&mut out, text);
}
RunEvent::ReasoningDelta(text) if self.reasoning => {
if !self.in_reasoning {
self.end_block(&mut out);
out.push_str("[reasoning] ");
self.line_open = true;
self.in_reasoning = true;
}
self.push_text(&mut out, text);
}
RunEvent::ToolCall(call) if self.tool_calls => {
self.end_block(&mut out);
out.push_str(&format!("[tool call] {}({})\n", call.name, call.arguments));
}
RunEvent::ToolResult {
tool,
content,
is_error,
..
} if self.tool_results => {
self.end_block(&mut out);
let label = if is_error {
"tool error"
} else {
"tool result"
};
out.push_str(&format!("[{label}] {tool}: {content}\n"));
}
RunEvent::Finished { .. } => self.end_block(&mut out),
_ => {}
}
out
}
fn push_text(&mut self, out: &mut String, text: &str) {
if text.is_empty() {
return;
}
out.push_str(text);
self.line_open = !text.ends_with('\n');
}
fn end_block(&mut self, out: &mut String) {
if self.line_open {
out.push('\n');
self.line_open = false;
}
self.in_reasoning = false;
}
}
impl<W: Write + Send> EventListener for Printer<W> {
fn on_event(&mut self, event: &RunEvent<'_>) -> Result<(), ListenerError> {
let text = self.render(event);
if !text.is_empty() {
self.out.write_all(text.as_bytes())?;
self.out.flush()?;
}
Ok(())
}
}
#[async_trait]
impl<W: futures_util::io::AsyncWrite + Unpin + Send> AsyncEventListener for Printer<W> {
async fn on_event(&mut self, event: &RunEvent<'_>) -> Result<(), ListenerError> {
use futures_util::io::AsyncWriteExt;
let text = self.render(event);
if !text.is_empty() {
self.out.write_all(text.as_bytes()).await?;
self.out.flush().await?;
}
Ok(())
}
}
#[cfg(feature = "tokio")]
mod tokio_writer {
use super::Printer;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
#[derive(Debug)]
pub struct TokioWriter<W>(W);
impl<W> TokioWriter<W> {
pub fn into_inner(self) -> W {
self.0
}
}
impl<W: tokio::io::AsyncWrite + Unpin> futures_util::io::AsyncWrite for TokioWriter<W> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.0).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.0).poll_flush(cx)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.0).poll_shutdown(cx)
}
}
impl<W> Printer<TokioWriter<W>> {
pub fn tokio(out: W) -> Self {
Printer::new(TokioWriter(out))
}
}
}
#[cfg(feature = "tokio")]
pub use tokio_writer::TokioWriter;
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::Usage;
fn print(printer: Printer<Vec<u8>>, events: &[RunEvent<'_>]) -> String {
let mut printer = printer;
for event in events {
EventListener::on_event(&mut printer, event).unwrap();
}
String::from_utf8(printer.into_inner()).unwrap()
}
fn finished(usage: &Usage) -> RunEvent<'_> {
RunEvent::Finished {
new_messages: &[],
usage,
stop_reason: None,
iterations: 1,
}
}
#[test]
fn reasoning_over_several_lines_has_one_prefix() {
let usage = Usage::default();
let output = print(
Printer::new(Vec::new()).with_reasoning(true),
&[
RunEvent::ReasoningDelta("a\n"),
RunEvent::ReasoningDelta("b"),
RunEvent::TextDelta("answer"),
finished(&usage),
],
);
assert_eq!(output, "[reasoning] a\nb\nanswer\n");
}
#[test]
fn reasoning_after_a_complete_line_starts_on_a_new_line() {
let usage = Usage::default();
let output = print(
Printer::new(Vec::new()).with_reasoning(true),
&[
RunEvent::TextDelta("intro\n"),
RunEvent::ReasoningDelta("think"),
RunEvent::TextDelta("done\n"),
finished(&usage),
],
);
assert_eq!(output, "intro\n[reasoning] think\ndone\n");
}
#[test]
fn hidden_reasoning_writes_nothing() {
let usage = Usage::default();
let output = print(
Printer::new(Vec::new()),
&[
RunEvent::ReasoningDelta("think"),
RunEvent::TextDelta("answer"),
finished(&usage),
],
);
assert_eq!(output, "answer\n");
}
}