use serde::Deserialize;
use crate::core::{ObservedDecision, ObservedStatus, ObservedStep};
pub const REVISION: &str = "acp/v1";
#[derive(Debug, Clone, Deserialize)]
pub struct Notification {
#[serde(rename = "sessionId")]
pub session_id: String,
pub update: Update,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Update {
#[serde(rename = "sessionUpdate")]
pub session_update: String,
#[serde(rename = "toolCallId", default)]
pub tool_call_id: Option<String>,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub title: Option<String>,
#[serde(rename = "stopReason", default)]
pub stop_reason: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PermissionRequest {
#[serde(rename = "sessionId")]
pub session_id: String,
#[serde(rename = "toolCall")]
pub tool_call: PermissionToolCall,
pub options: Vec<PermissionOption>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PermissionToolCall {
#[serde(rename = "toolCallId")]
pub tool_call_id: String,
#[serde(default)]
pub title: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PermissionOption {
#[serde(rename = "optionId")]
pub option_id: String,
pub kind: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PermissionResponse {
pub outcome: PermissionOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum PermissionOutcome {
Selected {
#[serde(rename = "optionId")]
option_id: String,
},
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mapped {
Step {
step: ObservedStep,
detail: Option<String>,
},
NotRecorded {
session_update: String,
},
}
#[must_use]
pub fn step_of(update: &Update) -> Mapped {
let unrecorded = || Mapped::NotRecorded {
session_update: update.session_update.clone(),
};
match update.session_update.as_str() {
"user_message_chunk" => Mapped::Step {
step: ObservedStep::Prompted,
detail: update.title.clone(),
},
"tool_call" | "tool_call_update" => {
let Some(call) = update.tool_call_id.clone() else {
return unrecorded();
};
Mapped::Step {
step: ObservedStep::ToolCall {
call,
status: status_of(update.status.as_deref()),
},
detail: update.title.clone(),
}
}
_ => unrecorded(),
}
}
#[must_use]
pub fn decision_of(request: &PermissionRequest, response: &PermissionResponse) -> Mapped {
let outcome = match &response.outcome {
PermissionOutcome::Cancelled => ObservedDecision::Cancelled,
PermissionOutcome::Selected { option_id } => ObservedDecision::Selected {
option: option_id.clone(),
kind: request
.options
.iter()
.find(|o| &o.option_id == option_id)
.map(|o| o.kind.clone())
.unwrap_or_default(),
},
};
Mapped::Step {
step: ObservedStep::Decision {
call: request.tool_call.tool_call_id.clone(),
outcome,
},
detail: request.tool_call.title.clone(),
}
}
#[must_use]
pub fn turn_ended(stop_reason: &str) -> Mapped {
Mapped::Step {
step: ObservedStep::TurnEnded {
reason: stop_reason.to_owned(),
},
detail: None,
}
}
fn status_of(status: Option<&str>) -> ObservedStatus {
match status {
Some("in_progress") => ObservedStatus::InProgress,
Some("completed") => ObservedStatus::Completed,
Some("failed") => ObservedStatus::Failed,
_ => ObservedStatus::Pending,
}
}