use serde::Serialize; use tracing::error; use tracing::warn; #[derive(Debug, Default)] pub(crate) struct UserNotifier { notify_command: Option>, } impl UserNotifier { pub(crate) fn notify(&self, notification: &UserNotification) { if let Some(notify_command) = &self.notify_command && !notify_command.is_empty() { self.invoke_notify(notify_command, notification) } } fn invoke_notify(&self, notify_command: &[String], notification: &UserNotification) { let Ok(json) = serde_json::to_string(¬ification) else { error!("failed to serialise notification payload"); return; }; let mut command = std::process::Command::new(¬ify_command[0]); if notify_command.len() > 1 { command.args(¬ify_command[1..]); } command.arg(json); // Fire-and-forget – we do not wait for completion. if let Err(e) = command.spawn() { warn!("failed to spawn notifier '{}': {e}", notify_command[0]); } } pub(crate) fn new(notify: Option>) -> Self { Self { notify_command: notify, } } } /// User can configure a program that will receive notifications. Each /// notification is serialized as JSON and passed as an argument to the /// program. #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(tag = "type", rename_all = "kebab-case")] pub(crate) enum UserNotification { #[serde(rename_all = "kebab-case")] AgentTurnComplete { thread_id: String, turn_id: String, cwd: String, /// Messages that the user sent to the agent to initiate the turn. input_messages: Vec, /// The last message sent by the assistant in the turn. last_assistant_message: Option, }, } #[cfg(test)] mod tests { use super::*; use anyhow::Result; #[test] fn test_user_notification() -> Result<()> { let notification = UserNotification::AgentTurnComplete { thread_id: "b5f6c1c2-1111-2222-3333-444455556666".to_string(), turn_id: "12345".to_string(), cwd: "/Users/example/project".to_string(), input_messages: vec!["Rename `foo` to `bar` and update the callsites.".to_string()], last_assistant_message: Some( "Rename complete and verified `cargo build` succeeds.".to_string(), ), }; let serialized = serde_json::to_string(¬ification)?; assert_eq!( serialized, r#"{"type":"agent-turn-complete","thread-id":"b5f6c1c2-1111-2222-3333-444455556666","turn-id":"12345","cwd":"/Users/example/project","input-messages":["Rename `foo` to `bar` and update the callsites."],"last-assistant-message":"Rename complete and verified `cargo build` succeeds."}"# ); Ok(()) } }