use super::resolve_session_scope_path;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum SessionEventKind {
UserInput,
AssistantChunk,
AssistantOutput,
ProviderResponseItem,
ReasoningSummary,
ToolCall,
ToolResult,
ToolResultAssessment,
ToolDisplayResult,
CodeModeToolCall,
CodeModeToolResult,
CodeModeContext,
CodeModeWarning,
CodeModeAssessment,
SubagentSession,
TaskScope,
ProviderContextItem,
HookDiagnostic,
HookLifecycle,
HookContextInjection,
TurnStatus,
AbortRecovery,
ProviderStreamTrace,
ContextCache,
SessionTitle,
Compaction,
Diagnostic,
Rewind,
SubdirInstructionLoad,
TtsrInjection,
SkillSuggestion,
SessionUsage,
SessionActiveTime,
SessionPreferences,
}
impl SessionEventKind {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::UserInput => "user_input",
Self::AssistantChunk => "assistant_chunk",
Self::AssistantOutput => "assistant_output",
Self::ProviderResponseItem => "provider_response_item",
Self::ReasoningSummary => "reasoning_summary",
Self::ToolCall => "tool_call",
Self::ToolResult => "tool_result",
Self::ToolResultAssessment => "tool_result_assessment",
Self::ToolDisplayResult => "tool_display_result",
Self::CodeModeToolCall => "code_mode_tool_call",
Self::CodeModeToolResult => "code_mode_tool_result",
Self::CodeModeContext => "code_mode_context",
Self::CodeModeWarning => "code_mode_warning",
Self::CodeModeAssessment => "code_mode_assessment",
Self::SubagentSession => "subagent_session",
Self::TaskScope => "task_scope",
Self::ProviderContextItem => "provider_context_item",
Self::HookDiagnostic => "hook_diagnostic",
Self::HookLifecycle => "hook_lifecycle",
Self::HookContextInjection => "hook_context_injection",
Self::TurnStatus => "turn_status",
Self::AbortRecovery => "abort_recovery",
Self::ProviderStreamTrace => "provider_stream_trace",
Self::ContextCache => "context_cache",
Self::SessionTitle => "session_title",
Self::Compaction => "compaction",
Self::Diagnostic => "diagnostic",
Self::Rewind => "rewind",
Self::SubdirInstructionLoad => "subdir_instruction_load",
Self::TtsrInjection => "ttsr_injection",
Self::SkillSuggestion => "skill_suggestion",
Self::SessionUsage => "session_usage",
Self::SessionActiveTime => "session_active_time",
Self::SessionPreferences => "session_preferences",
}
}
fn parse(value: &str) -> Option<Self> {
match value {
"user_input" => Some(Self::UserInput),
"assistant_chunk" => Some(Self::AssistantChunk),
"assistant_output" => Some(Self::AssistantOutput),
"provider_response_item" => Some(Self::ProviderResponseItem),
"reasoning_summary" => Some(Self::ReasoningSummary),
"tool_call" => Some(Self::ToolCall),
"tool_result" => Some(Self::ToolResult),
"tool_result_assessment" => Some(Self::ToolResultAssessment),
"tool_display_result" => Some(Self::ToolDisplayResult),
"code_mode_tool_call" => Some(Self::CodeModeToolCall),
"code_mode_tool_result" => Some(Self::CodeModeToolResult),
"code_mode_context" => Some(Self::CodeModeContext),
"code_mode_warning" => Some(Self::CodeModeWarning),
"code_mode_assessment" => Some(Self::CodeModeAssessment),
"subagent_session" => Some(Self::SubagentSession),
"task_scope" => Some(Self::TaskScope),
"provider_context_item" => Some(Self::ProviderContextItem),
"hook_diagnostic" => Some(Self::HookDiagnostic),
"hook_lifecycle" => Some(Self::HookLifecycle),
"hook_context_injection" => Some(Self::HookContextInjection),
"turn_status" => Some(Self::TurnStatus),
"abort_recovery" => Some(Self::AbortRecovery),
"provider_stream_trace" => Some(Self::ProviderStreamTrace),
"context_cache" => Some(Self::ContextCache),
"session_title" => Some(Self::SessionTitle),
"compaction" => Some(Self::Compaction),
"diagnostic" => Some(Self::Diagnostic),
"rewind" => Some(Self::Rewind),
"subdir_instruction_load" => Some(Self::SubdirInstructionLoad),
"ttsr_injection" => Some(Self::TtsrInjection),
"skill_suggestion" => Some(Self::SkillSuggestion),
"session_usage" => Some(Self::SessionUsage),
"session_active_time" => Some(Self::SessionActiveTime),
"session_preferences" => Some(Self::SessionPreferences),
_ => None,
}
}
pub(crate) const fn is_local_only(self) -> bool {
matches!(
self,
Self::HookDiagnostic
| Self::ToolDisplayResult
| Self::CodeModeToolCall
| Self::CodeModeToolResult
| Self::CodeModeContext
| Self::CodeModeWarning
| Self::CodeModeAssessment
| Self::ToolResultAssessment
| Self::SubagentSession
| Self::TaskScope
| Self::HookLifecycle
| Self::HookContextInjection
| Self::TurnStatus
| Self::AbortRecovery
| Self::ProviderStreamTrace
| Self::ContextCache
| Self::SessionTitle
| Self::Diagnostic
| Self::Rewind
| Self::SubdirInstructionLoad
| Self::TtsrInjection
| Self::SkillSuggestion
| Self::SessionUsage
| Self::SessionActiveTime
| Self::SessionPreferences
)
}
}
impl AsRef<str> for SessionEventKind {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum TurnStatus {
Incomplete,
#[serde(alias = "canceled")]
Cancelled,
Failed,
CompactionRequired,
Complete,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct TurnStatusPayload {
pub(crate) status: TurnStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) assistant_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) error_summary: Option<String>,
}
#[derive(Deserialize)]
struct RawTurnStatusPayload {
status: TurnStatus,
#[serde(default)]
assistant_text: Option<String>,
#[serde(default)]
error_summary: Option<String>,
}
impl<'de> Deserialize<'de> for TurnStatusPayload {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawTurnStatusPayload::deserialize(deserializer)?;
Ok(Self::new_with_error_summary(
raw.status,
raw.assistant_text.as_deref(),
raw.error_summary.as_deref(),
))
}
}
const TURN_STATUS_ERROR_SUMMARY_MAX_CHARS: usize = 2_000;
fn sanitize_turn_status_error_summary(summary: &str) -> Option<String> {
let sanitized = crate::output::sanitize_display_text(summary);
let trimmed = sanitized.trim();
(!trimmed.is_empty()).then(|| {
trimmed
.chars()
.take(TURN_STATUS_ERROR_SUMMARY_MAX_CHARS)
.collect()
})
}
impl TurnStatusPayload {
pub(crate) fn new(status: TurnStatus, assistant_text: Option<&str>) -> Self {
Self::new_with_error_summary(status, assistant_text, None)
}
pub(crate) fn new_with_error_summary(
status: TurnStatus,
assistant_text: Option<&str>,
error_summary: Option<&str>,
) -> Self {
Self {
status,
assistant_text: assistant_text.map(str::to_string),
error_summary: (status == TurnStatus::Failed)
.then(|| error_summary.and_then(sanitize_turn_status_error_summary))
.flatten(),
}
}
pub(crate) fn into_value(self) -> serde_json::Result<Value> {
serde_json::to_value(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct SessionEvent {
pub(crate) event_type: String,
pub(crate) timestamp: DateTime<Utc>,
pub(crate) session_id: String,
#[serde(default)]
pub(crate) cwd: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) session_path: Option<PathBuf>,
pub(crate) payload: Value,
}
impl SessionEvent {
pub(crate) fn new(
event_type: impl Into<String>,
session_id: String,
cwd: PathBuf,
payload: Value,
) -> Self {
Self {
event_type: event_type.into(),
timestamp: Utc::now(),
session_id,
cwd: cwd.clone(),
session_path: Some(resolve_session_scope_path(&cwd)),
payload,
}
}
pub(crate) fn new_kind(
kind: SessionEventKind,
session_id: String,
cwd: PathBuf,
payload: Value,
) -> Self {
Self::new(kind.as_str(), session_id, cwd, payload)
}
pub(crate) fn kind(&self) -> Option<SessionEventKind> {
SessionEventKind::parse(&self.event_type)
}
pub(crate) fn turn_status_payload(&self) -> Option<TurnStatusPayload> {
if self.kind() != Some(SessionEventKind::TurnStatus) {
return None;
}
let status = self
.payload
.get("status")
.cloned()
.and_then(|value| serde_json::from_value::<TurnStatus>(value).ok())?;
let assistant_text = self
.payload
.get("assistant_text")
.and_then(Value::as_str)
.map(str::to_string);
let error_summary = (status == TurnStatus::Failed)
.then(|| {
self.payload
.get("error_summary")
.and_then(Value::as_str)
.and_then(sanitize_turn_status_error_summary)
})
.flatten();
Some(TurnStatusPayload {
status,
assistant_text,
error_summary,
})
}
}