use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::id::ControlSessionId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BackendKind {
StreamJson,
Tmux,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum StopReason {
Requested,
Forced,
Completed,
MaxRestartsExceeded,
AuthFailed,
}
impl std::fmt::Display for StopReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Requested => f.write_str("requested"),
Self::Forced => f.write_str("forced"),
Self::Completed => f.write_str("completed"),
Self::MaxRestartsExceeded => f.write_str("max-restarts-exceeded"),
Self::AuthFailed => f.write_str("auth-failed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamJsonEvent {
pub event_type: String,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ActivityKind {
AwaitingInput,
ToolUse { name: String },
Error { excerpt: String },
TestResult { passed: u32, failed: u32 },
GitOp { verb: String },
AuthPrompt,
SessionComplete,
RateLimited,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SessionEvent {
Started {
session_id: ControlSessionId,
backend: BackendKind,
ts: DateTime<Utc>,
},
Output {
session_id: ControlSessionId,
raw: String,
structured: Option<StreamJsonEvent>,
ts: DateTime<Utc>,
},
ActivityParsed {
session_id: ControlSessionId,
kind: ActivityKind,
summary: String,
ts: DateTime<Utc>,
},
PendingDecision {
session_id: ControlSessionId,
prompt: String,
proposed_default: Option<String>,
ts: DateTime<Utc>,
},
Restarted {
session_id: ControlSessionId,
attempt: u8,
context_lost: bool,
ts: DateTime<Utc>,
},
ObserverLagged {
session_id: ControlSessionId,
dropped: u64,
},
ObserverCriticalLag {
session_id: ControlSessionId,
dropped: u64,
},
AuthFailed {
session_id: ControlSessionId,
backend: BackendKind,
reason: String,
ts: DateTime<Utc>,
},
Stopped {
session_id: ControlSessionId,
reason: StopReason,
ts: DateTime<Utc>,
},
Failed {
session_id: ControlSessionId,
reason: String,
ts: DateTime<Utc>,
},
}
impl SessionEvent {
pub fn session_id(&self) -> &ControlSessionId {
match self {
Self::Started { session_id, .. }
| Self::Output { session_id, .. }
| Self::ActivityParsed { session_id, .. }
| Self::PendingDecision { session_id, .. }
| Self::Restarted { session_id, .. }
| Self::ObserverLagged { session_id, .. }
| Self::ObserverCriticalLag { session_id, .. }
| Self::AuthFailed { session_id, .. }
| Self::Stopped { session_id, .. }
| Self::Failed { session_id, .. } => session_id,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stop_reason_display() {
assert_eq!(StopReason::Requested.to_string(), "requested");
assert_eq!(StopReason::Completed.to_string(), "completed");
assert_eq!(
StopReason::MaxRestartsExceeded.to_string(),
"max-restarts-exceeded"
);
assert_eq!(StopReason::AuthFailed.to_string(), "auth-failed");
}
#[test]
fn session_event_serialize() {
let id = ControlSessionId::new("my-proj", 0);
let event = SessionEvent::Started {
session_id: id.clone(),
backend: BackendKind::StreamJson,
ts: Utc::now(),
};
let json = serde_json::to_string(&event).expect("serialize");
let back: SessionEvent = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.session_id(), &id);
}
#[test]
fn session_event_session_id_accessor() {
let id = ControlSessionId::new("p", 5);
let event = SessionEvent::Failed {
session_id: id.clone(),
reason: "test".into(),
ts: Utc::now(),
};
assert_eq!(event.session_id(), &id);
}
#[test]
fn backend_kind_serializes_kebab_case() {
let json = serde_json::to_string(&BackendKind::StreamJson).unwrap();
assert_eq!(json, "\"stream-json\"");
let json = serde_json::to_string(&BackendKind::Tmux).unwrap();
assert_eq!(json, "\"tmux\"");
}
}