Skip to main content

chimera_core/
action.rs

1use serde::{Deserialize, Serialize};
2use strum::{Display, EnumString};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(tag = "kind", rename_all = "snake_case")]
6#[non_exhaustive]
7pub enum Action {
8    Command {
9        command: String,
10        #[serde(default)]
11        output: String,
12        exit_code: Option<i32>,
13        status: ActionStatus,
14    },
15    FileChange {
16        changes: Vec<FileChange>,
17        status: ActionStatus,
18    },
19    McpToolCall {
20        server: String,
21        tool: String,
22        #[serde(default)]
23        arguments: serde_json::Value,
24        result: Option<serde_json::Value>,
25        error: Option<String>,
26        status: ActionStatus,
27    },
28    WebSearch {
29        query: String,
30    },
31    ToolUse {
32        name: String,
33        #[serde(default)]
34        input: serde_json::Value,
35        result: Option<serde_json::Value>,
36        #[serde(default)]
37        is_error: bool,
38    },
39    Collaboration {
40        tool: String,
41        #[serde(default)]
42        agents: serde_json::Value,
43        status: ActionStatus,
44    },
45    Other {
46        name: String,
47        #[serde(default)]
48        data: serde_json::Value,
49    },
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
53#[serde(rename_all = "snake_case")]
54#[strum(serialize_all = "snake_case")]
55#[non_exhaustive]
56pub enum ActionStatus {
57    InProgress,
58    Completed,
59    Failed,
60    Declined,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct FileChange {
65    pub path: String,
66    pub kind: FileChangeKind,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
70#[serde(rename_all = "snake_case")]
71#[strum(serialize_all = "snake_case")]
72#[non_exhaustive]
73pub enum FileChangeKind {
74    Add,
75    Delete,
76    Update,
77}
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize)]
80pub struct Usage {
81    #[serde(default)]
82    pub input_tokens: Option<u64>,
83    #[serde(default)]
84    pub output_tokens: Option<u64>,
85    #[serde(default)]
86    pub cache_read_tokens: Option<u64>,
87    #[serde(default)]
88    pub cache_creation_tokens: Option<u64>,
89    #[serde(default)]
90    pub total_cost_usd: Option<f64>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
94#[serde(rename_all = "snake_case")]
95#[strum(serialize_all = "snake_case")]
96pub enum TurnStatus {
97    Completed,
98    Interrupted,
99    Failed,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(tag = "kind", rename_all = "snake_case")]
104pub enum TurnProviderMeta {
105    #[serde(rename = "claude_code")]
106    ClaudeCode {
107        #[serde(default)]
108        stop_reason: Option<String>,
109    },
110    #[serde(rename = "opencode")]
111    OpenCode {
112        #[serde(default)]
113        finish_reason: Option<String>,
114    },
115}
116
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
118pub struct TurnResult {
119    pub text: Option<String>,
120    pub duration_ms: Option<u64>,
121    pub session_id: Option<String>,
122    pub num_turns: Option<u32>,
123    #[serde(default)]
124    pub turn_status: Option<TurnStatus>,
125    #[serde(default)]
126    pub provider_meta: Option<TurnProviderMeta>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct PlanItem {
131    pub text: String,
132    pub completed: bool,
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use std::str::FromStr;
139
140    #[test]
141    fn action_status_display() {
142        assert_eq!(ActionStatus::InProgress.to_string(), "in_progress");
143        assert_eq!(ActionStatus::Completed.to_string(), "completed");
144        assert_eq!(ActionStatus::Failed.to_string(), "failed");
145        assert_eq!(ActionStatus::Declined.to_string(), "declined");
146    }
147
148    #[test]
149    fn action_status_from_str() {
150        assert_eq!(
151            ActionStatus::from_str("in_progress").unwrap(),
152            ActionStatus::InProgress
153        );
154        assert_eq!(
155            ActionStatus::from_str("completed").unwrap(),
156            ActionStatus::Completed
157        );
158    }
159
160    #[test]
161    fn file_change_kind_roundtrip() {
162        assert_eq!(
163            FileChangeKind::from_str("add").unwrap(),
164            FileChangeKind::Add
165        );
166        assert_eq!(FileChangeKind::Delete.to_string(), "delete");
167    }
168
169    #[test]
170    fn action_serde_roundtrip() {
171        let action = Action::Command {
172            command: "ls -la".into(),
173            output: "total 0".into(),
174            exit_code: Some(0),
175            status: ActionStatus::Completed,
176        };
177
178        let json = serde_json::to_string(&action).unwrap();
179        let parsed: Action = serde_json::from_str(&json).unwrap();
180
181        match parsed {
182            Action::Command {
183                command,
184                exit_code,
185                status,
186                ..
187            } => {
188                assert_eq!(command, "ls -la");
189                assert_eq!(exit_code, Some(0));
190                assert_eq!(status, ActionStatus::Completed);
191            }
192            _ => panic!("expected Command variant"),
193        }
194    }
195
196    #[test]
197    fn usage_defaults() {
198        let usage: Usage = serde_json::from_str("{}").unwrap();
199        assert!(usage.input_tokens.is_none());
200        assert!(usage.output_tokens.is_none());
201        assert!(usage.total_cost_usd.is_none());
202    }
203
204    #[test]
205    fn turn_status_roundtrip() {
206        assert_eq!(
207            TurnStatus::from_str("completed").unwrap(),
208            TurnStatus::Completed
209        );
210        assert_eq!(TurnStatus::Interrupted.to_string(), "interrupted");
211    }
212
213    #[test]
214    fn turn_result_defaults() {
215        let result: TurnResult = serde_json::from_str("{}").unwrap();
216        assert!(result.text.is_none());
217        assert!(result.turn_status.is_none());
218        assert!(result.provider_meta.is_none());
219    }
220
221    #[test]
222    fn turn_provider_meta_roundtrip() {
223        let meta = TurnProviderMeta::ClaudeCode {
224            stop_reason: Some("end_turn".into()),
225        };
226        let json = serde_json::to_string(&meta).unwrap();
227        let parsed: TurnProviderMeta = serde_json::from_str(&json).unwrap();
228        match parsed {
229            TurnProviderMeta::ClaudeCode { stop_reason } => {
230                assert_eq!(stop_reason.as_deref(), Some("end_turn"));
231            }
232            _ => panic!("expected ClaudeCode metadata"),
233        }
234    }
235}