use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Action {
Command {
command: String,
#[serde(default)]
output: String,
exit_code: Option<i32>,
status: ActionStatus,
},
FileChange {
changes: Vec<FileChange>,
status: ActionStatus,
},
McpToolCall {
server: String,
tool: String,
#[serde(default)]
arguments: serde_json::Value,
result: Option<serde_json::Value>,
error: Option<String>,
status: ActionStatus,
},
WebSearch {
query: String,
},
ToolUse {
name: String,
#[serde(default)]
input: serde_json::Value,
result: Option<serde_json::Value>,
#[serde(default)]
is_error: bool,
},
Collaboration {
tool: String,
#[serde(default)]
agents: serde_json::Value,
status: ActionStatus,
},
Other {
name: String,
#[serde(default)]
data: serde_json::Value,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
#[non_exhaustive]
pub enum ActionStatus {
InProgress,
Completed,
Failed,
Declined,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileChange {
pub path: String,
pub kind: FileChangeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
#[non_exhaustive]
pub enum FileChangeKind {
Add,
Delete,
Update,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Usage {
#[serde(default)]
pub input_tokens: Option<u64>,
#[serde(default)]
pub output_tokens: Option<u64>,
#[serde(default)]
pub cache_read_tokens: Option<u64>,
#[serde(default)]
pub cache_creation_tokens: Option<u64>,
#[serde(default)]
pub total_cost_usd: Option<f64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TurnResult {
pub text: Option<String>,
pub duration_ms: Option<u64>,
pub session_id: Option<String>,
pub num_turns: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanItem {
pub text: String,
pub completed: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn action_status_display() {
assert_eq!(ActionStatus::InProgress.to_string(), "in_progress");
assert_eq!(ActionStatus::Completed.to_string(), "completed");
assert_eq!(ActionStatus::Failed.to_string(), "failed");
assert_eq!(ActionStatus::Declined.to_string(), "declined");
}
#[test]
fn action_status_from_str() {
assert_eq!(
ActionStatus::from_str("in_progress").unwrap(),
ActionStatus::InProgress
);
assert_eq!(
ActionStatus::from_str("completed").unwrap(),
ActionStatus::Completed
);
}
#[test]
fn file_change_kind_roundtrip() {
assert_eq!(
FileChangeKind::from_str("add").unwrap(),
FileChangeKind::Add
);
assert_eq!(FileChangeKind::Delete.to_string(), "delete");
}
#[test]
fn action_serde_roundtrip() {
let action = Action::Command {
command: "ls -la".into(),
output: "total 0".into(),
exit_code: Some(0),
status: ActionStatus::Completed,
};
let json = serde_json::to_string(&action).unwrap();
let parsed: Action = serde_json::from_str(&json).unwrap();
match parsed {
Action::Command {
command,
exit_code,
status,
..
} => {
assert_eq!(command, "ls -la");
assert_eq!(exit_code, Some(0));
assert_eq!(status, ActionStatus::Completed);
}
_ => panic!("expected Command variant"),
}
}
#[test]
fn usage_defaults() {
let usage: Usage = serde_json::from_str("{}").unwrap();
assert!(usage.input_tokens.is_none());
assert!(usage.output_tokens.is_none());
assert!(usage.total_cost_usd.is_none());
}
}