use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::bootstrap::state::{get_is_non_interactive_session, get_session_id};
const MAX_QUEUE_SIZE: usize = 1000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SdkEventType {
TaskStarted,
TaskProgress,
TaskNotification,
SessionStateChanged,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SdkEvent {
#[serde(rename = "type")]
pub event_type: String,
pub subtype: SdkEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_use_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<SdkEventUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow_progress: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SdkEventUsage {
pub total_tokens: u64,
pub tool_uses: u64,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DrainedSdkEvent {
pub uuid: String,
pub session_id: String,
#[serde(flatten)]
pub event: SdkEvent,
}
impl SdkEvent {
pub fn task_started(
task_id: String,
tool_use_id: Option<String>,
description: String,
task_type: Option<String>,
workflow_name: Option<String>,
prompt: Option<String>,
) -> Self {
Self {
event_type: "system".to_string(),
subtype: SdkEventType::TaskStarted,
task_id: Some(task_id),
tool_use_id,
description: Some(description),
task_type,
workflow_name,
prompt,
usage: None,
last_tool_name: None,
summary: None,
workflow_progress: None,
status: None,
output_file: None,
state: None,
}
}
pub fn task_progress(
task_id: String,
tool_use_id: Option<String>,
description: String,
usage: SdkEventUsage,
last_tool_name: Option<String>,
summary: Option<String>,
workflow_progress: Option<Vec<serde_json::Value>>,
) -> Self {
Self {
event_type: "system".to_string(),
subtype: SdkEventType::TaskProgress,
task_id: Some(task_id),
tool_use_id,
description: Some(description),
task_type: None,
workflow_name: None,
prompt: None,
usage: Some(usage),
last_tool_name,
summary,
workflow_progress,
status: None,
output_file: None,
state: None,
}
}
pub fn task_notification(
task_id: String,
tool_use_id: Option<String>,
status: String,
output_file: String,
summary: String,
usage: Option<SdkEventUsage>,
) -> Self {
Self {
event_type: "system".to_string(),
subtype: SdkEventType::TaskNotification,
task_id: Some(task_id),
tool_use_id,
description: None,
task_type: None,
workflow_name: None,
prompt: None,
usage,
last_tool_name: None,
summary: if summary.is_empty() {
None
} else {
Some(summary)
},
workflow_progress: None,
status: Some(status),
output_file: if output_file.is_empty() {
None
} else {
Some(output_file)
},
state: None,
}
}
pub fn session_state_changed(state: String) -> Self {
Self {
event_type: "system".to_string(),
subtype: SdkEventType::SessionStateChanged,
task_id: None,
tool_use_id: None,
description: None,
task_type: None,
workflow_name: None,
prompt: None,
usage: None,
last_tool_name: None,
summary: None,
workflow_progress: None,
status: None,
output_file: None,
state: Some(state),
}
}
}
pub struct SdkEventQueue {
events: Arc<Mutex<VecDeque<SdkEvent>>>,
}
impl SdkEventQueue {
pub fn new() -> Self {
Self {
events: Arc::new(Mutex::new(VecDeque::with_capacity(MAX_QUEUE_SIZE))),
}
}
pub fn push(&self, event: SdkEvent) {
let mut queue = self.events.lock().unwrap();
if queue.len() >= MAX_QUEUE_SIZE {
queue.pop_front();
}
queue.push_back(event);
}
pub fn pop(&self) -> Option<SdkEvent> {
self.events.lock().unwrap().pop_front()
}
pub fn drain(&self) -> Vec<SdkEvent> {
self.events.lock().unwrap().drain(..).collect()
}
pub fn len(&self) -> usize {
self.events.lock().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
self.events.lock().unwrap().clear();
}
}
impl Default for SdkEventQueue {
fn default() -> Self {
Self::new()
}
}
static GLOBAL_QUEUE: std::sync::LazyLock<SdkEventQueue> =
std::sync::LazyLock::new(SdkEventQueue::new);
pub fn enqueue_sdk_event(event: SdkEvent) {
if !get_is_non_interactive_session() {
return;
}
GLOBAL_QUEUE.push(event);
}
pub fn drain_sdk_events() -> Vec<DrainedSdkEvent> {
let events = GLOBAL_QUEUE.drain();
if events.is_empty() {
return Vec::new();
}
let session_id = get_session_id();
events
.into_iter()
.map(|event| DrainedSdkEvent {
uuid: Uuid::new_v4().to_string(),
session_id: session_id.clone(),
event,
})
.collect()
}
pub fn emit_task_started(
task_id: &str,
tool_use_id: Option<String>,
description: &str,
task_type: Option<String>,
workflow_name: Option<String>,
prompt: Option<String>,
) {
enqueue_sdk_event(SdkEvent::task_started(
task_id.to_string(),
tool_use_id,
description.to_string(),
task_type,
workflow_name,
prompt,
));
}
pub fn emit_task_progress(params: TaskProgressParams) {
enqueue_sdk_event(SdkEvent::task_progress(
params.task_id,
params.tool_use_id,
params.description,
params.usage,
params.last_tool_name,
params.summary,
params.workflow_progress,
));
}
pub struct TaskProgressParams {
pub task_id: String,
pub tool_use_id: Option<String>,
pub description: String,
pub usage: SdkEventUsage,
pub last_tool_name: Option<String>,
pub summary: Option<String>,
pub workflow_progress: Option<Vec<serde_json::Value>>,
}
pub fn emit_task_terminated_sdk(
task_id: &str,
tool_use_id: Option<String>,
status: &str,
summary: Option<String>,
output_file: Option<String>,
usage: Option<SdkEventUsage>,
) {
enqueue_sdk_event(SdkEvent::task_notification(
task_id.to_string(),
tool_use_id,
status.to_string(),
output_file.unwrap_or_default(),
summary.unwrap_or_default(),
usage,
));
}
pub fn emit_session_state_changed(state: &str) {
enqueue_sdk_event(SdkEvent::session_state_changed(state.to_string()));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sdk_event_task_started() {
let event = SdkEvent::task_started(
"task-1".to_string(),
Some("tool-1".to_string()),
"Test task".to_string(),
Some("local_agent".to_string()),
None,
None,
);
assert_eq!(event.subtype, SdkEventType::TaskStarted);
assert_eq!(event.task_id, Some("task-1".to_string()));
assert_eq!(event.event_type, "system");
}
#[test]
fn test_sdk_event_task_notification() {
let event = SdkEvent::task_notification(
"task-1".to_string(),
None,
"completed".to_string(),
"/tmp/task_output.txt".to_string(),
"Task completed successfully".to_string(),
None,
);
assert_eq!(event.subtype, SdkEventType::TaskNotification);
assert_eq!(event.status, Some("completed".to_string()));
}
#[test]
fn test_sdk_event_session_state_changed() {
let event = SdkEvent::session_state_changed("idle".to_string());
assert_eq!(event.subtype, SdkEventType::SessionStateChanged);
assert_eq!(event.state, Some("idle".to_string()));
}
#[test]
fn test_sdk_event_queue() {
let queue = SdkEventQueue::new();
let event = SdkEvent::task_started(
"task-1".to_string(),
None,
"Test".to_string(),
None,
None,
None,
);
queue.push(event.clone());
assert_eq!(queue.len(), 1);
assert_eq!(queue.pop(), Some(event));
assert_eq!(queue.len(), 0);
}
#[test]
fn test_sdk_event_queue_capacity() {
let queue = SdkEventQueue::new();
for i in 0..1000 {
queue.push(SdkEvent::task_started(
format!("task-{i}"),
None,
"Test".to_string(),
None,
None,
None,
));
}
assert_eq!(queue.len(), 1000);
}
#[test]
fn test_sdk_event_usage() {
let usage = SdkEventUsage {
total_tokens: 1500,
tool_uses: 5,
duration_ms: 3000,
};
assert_eq!(usage.total_tokens, 1500);
assert_eq!(usage.tool_uses, 5);
assert_eq!(usage.duration_ms, 3000);
}
}