1use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(tag = "action", rename_all = "snake_case")]
14#[non_exhaustive]
15pub enum Action {
16 RunExperiment {
18 name: String,
20 research_line: String,
22 hypothesis: String,
28 params: BTreeMap<String, serde_json::Value>,
30 #[serde(default)]
32 parent: Option<String>,
33 },
34
35 Conclude {
37 reason: String,
39 },
40}
41
42impl Action {
43 pub fn response_schema() -> serde_json::Value {
49 serde_json::json!({
50 "type": "object",
51 "properties": {
52 "action": {
53 "type": "string",
54 "enum": ["run_experiment", "conclude"]
55 },
56 "name": {"type": "string"},
57 "research_line": {"type": "string"},
58 "hypothesis": {"type": "string"},
59 "params": {"type": "object"},
60 "parent": {"type": ["string", "null"]},
61 "reason": {"type": "string"}
62 },
63 "required": ["action"]
64 })
65 }
66
67 pub fn is_terminal(&self) -> bool {
69 matches!(self, Self::Conclude { .. })
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use serde_json::json;
77
78 #[test]
79 fn an_experiment_round_trips() {
80 let action: Action = serde_json::from_value(json!({
81 "action": "run_experiment",
82 "name": "exp_0001",
83 "research_line": "regularization",
84 "hypothesis": "stronger L2 lifts held-out F1 above 0.8",
85 "params": {"classifier.C": 0.1}
86 }))
87 .unwrap();
88
89 match &action {
90 Action::RunExperiment {
91 name,
92 params,
93 parent,
94 ..
95 } => {
96 assert_eq!(name, "exp_0001");
97 assert_eq!(params["classifier.C"], json!(0.1));
98 assert!(parent.is_none(), "parent is optional");
99 }
100 other => panic!("{other:?}"),
101 }
102 assert!(!action.is_terminal());
103 }
104
105 #[test]
106 fn a_conclusion_is_terminal() {
107 let action: Action =
108 serde_json::from_value(json!({"action": "conclude", "reason": "plateaued"})).unwrap();
109 assert!(action.is_terminal());
110 }
111
112 #[test]
114 fn an_experiment_without_a_hypothesis_is_rejected() {
115 let result: std::result::Result<Action, _> = serde_json::from_value(json!({
116 "action": "run_experiment",
117 "name": "exp",
118 "research_line": "l",
119 "params": {}
120 }));
121 assert!(result.is_err());
122 }
123}