use std::io::Write;
use std::sync::Mutex;
use kodact_engine::{EngineEvent, EventSink, NoticeLevel, TurnResult};
use kodact_types::sdk::{
AssistantMessage, ResultMessage, SdkMessage, StreamEventMessage, SystemMessage, ToolProgress, UserMessage,
UserPayload,
};
use kodact_types::{new_uuid, MessageContent, Role};
use serde_json::Value;
#[derive(Default)]
pub struct Out {
lock: Mutex<()>,
}
impl Out {
pub fn line(&self, msg: &SdkMessage) {
let _g = self.lock.lock().unwrap();
stdout_line(format_args!("{}", msg.to_line()));
}
}
pub fn stdout_line(args: std::fmt::Arguments) {
let mut so = std::io::stdout().lock();
let r = so.write_fmt(args).and_then(|_| so.write_all(b"\n")).and_then(|_| so.flush());
if let Err(e) = r {
if e.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(crate::exit::OK);
}
}
}
pub fn result_message(r: &TurnResult, session_id: &str) -> ResultMessage {
ResultMessage {
subtype: r.subtype,
is_error: r.is_error,
duration_ms: r.duration_ms,
duration_api_ms: r.duration_api_ms,
num_turns: r.num_turns,
result: r.result.clone().or_else(|| r.prompt_blocked.clone()),
stop_reason: r.stop_reason.clone(),
session_id: session_id.to_string(),
total_cost_usd: r.total_cost_usd,
usage: r.usage.clone(),
model_usage: r.model_usage.clone(),
permission_denials: r.permission_denials.clone(),
structured_output: r.structured_output.clone(),
errors: r.errors.clone(),
uuid: new_uuid(),
immediate: None,
}
}
pub struct StreamSink {
pub out: std::sync::Arc<Out>,
pub session_id: std::sync::Arc<Mutex<String>>,
pub include_partial: bool,
pub replay_user_messages: bool,
pub verbose: bool,
}
impl EventSink for StreamSink {
fn emit(&self, event: EngineEvent) {
let sid = self.session_id.lock().unwrap().clone();
let msg = match event {
EngineEvent::Stream { event, parent_tool_use_id } => {
if !self.include_partial {
return;
}
SdkMessage::StreamEvent(StreamEventMessage {
event,
parent_tool_use_id,
session_id: sid,
uuid: new_uuid(),
})
}
EngineEvent::PromptAccepted { message, uuid } => {
if !self.replay_user_messages {
return;
}
SdkMessage::User(UserMessage {
message: UserPayload { role: Role::User, content: MessageContent::Blocks(message.content) },
parent_tool_use_id: None,
session_id: sid,
uuid: Some(uuid),
is_replay: Some(true),
is_synthetic: None,
tool_use_result: None,
priority: None,
})
}
EngineEvent::Assistant { message, uuid, parent_tool_use_id } => SdkMessage::Assistant(AssistantMessage {
message,
parent_tool_use_id,
session_id: sid,
uuid,
error: None,
}),
EngineEvent::User { message, uuid, tool_use_result, is_meta, parent_tool_use_id } => {
SdkMessage::User(UserMessage {
message: UserPayload { role: Role::User, content: MessageContent::Blocks(message.content) },
parent_tool_use_id,
session_id: sid,
uuid: Some(uuid),
is_replay: None,
is_synthetic: is_meta.then_some(true),
tool_use_result,
priority: None,
})
}
EngineEvent::System { subtype, mut data } => {
if let Value::Object(m) = &mut data {
m.insert("session_id".into(), Value::String(sid));
m.insert("uuid".into(), Value::String(new_uuid()));
}
SdkMessage::System(SystemMessage::new(&subtype, data))
}
EngineEvent::ToolProgress { tool_use_id, tool_name, elapsed_secs } => {
SdkMessage::ToolProgress(ToolProgress {
tool_use_id,
tool_name,
parent_tool_use_id: None,
elapsed_time_seconds: elapsed_secs,
session_id: sid,
uuid: new_uuid(),
})
}
EngineEvent::Notice { level, text } => {
if self.verbose || level == NoticeLevel::Error {
eprintln!("{text}");
}
return;
}
};
self.out.line(&msg);
}
}
pub struct QuietSink {
pub verbose: bool,
pub quiet: bool,
}
impl EventSink for QuietSink {
fn emit(&self, event: EngineEvent) {
let EngineEvent::Notice { level, text } = event else { return };
let show = match level {
NoticeLevel::Error => false,
NoticeLevel::Warning => !self.quiet,
NoticeLevel::Info => self.verbose,
};
if show {
eprintln!("{} {text}", crate::term::yellow("kodact:"));
}
}
}