rightkit_agent_engine_protocol/
schedule.rs1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13pub fn default_job_mode() -> String {
14 "agent".to_string()
15}
16
17pub fn default_schema_version() -> u32 {
18 1
19}
20
21#[derive(Clone, Debug, Default, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct ScheduleRunState {
24 #[serde(default)]
25 pub last_claimed_minute: HashMap<String, i64>,
26 #[serde(default)]
27 pub history: Vec<ScheduleRunEvent>,
28}
29
30#[derive(Clone, Debug, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct ScheduleRunEvent {
33 #[serde(default = "default_schema_version")]
34 pub schema_version: u32,
35 pub schedule_id: String,
36 pub epoch_minute: i64,
37 pub status: String,
38 pub session_id: String,
39 pub message: String,
40 #[serde(default, skip_serializing_if = "String::is_empty")]
41 pub started_at: String,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub finished_at: Option<String>,
44 #[serde(default = "default_job_mode")]
45 pub mode: String,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub toolset: Option<String>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub execution_backend: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub resolved_execution_backend: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub execution_workspace_id: Option<String>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub execution_workspace_path: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub resource_limits: Option<ScheduleRunResourceLimits>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub workdir: Option<String>,
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
61 pub skills: Vec<String>,
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub delivery_kinds: Vec<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub source_recipe_id: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub exit_code: Option<i32>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub artifact_path: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub delivery_status: Option<String>,
72 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub delivery_errors: Vec<String>,
74}
75
76#[derive(Clone, Debug, Default, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct ScheduleRunResourceLimits {
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub cpu: Option<u32>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub memory_mb: Option<u32>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub disk_mb: Option<u32>,
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 const LEGACY_FULL: &str = r#"{
94 "schemaVersion": 1,
95 "scheduleId": "sched-abc123",
96 "epochMinute": 28521600,
97 "status": "completed",
98 "sessionId": "sess-xyz789",
99 "message": "run finished ok",
100 "startedAt": "2026-06-21T10:00:00Z",
101 "finishedAt": "2026-06-21T10:02:00Z",
102 "mode": "agent",
103 "toolset": "default",
104 "executionBackend": "local",
105 "resolvedExecutionBackend": "local",
106 "executionWorkspaceId": "ws-1",
107 "executionWorkspacePath": "/home/user/ws",
108 "resourceLimits": { "cpu": 2, "memoryMb": 4096, "diskMb": 8192 },
109 "workdir": "/home/user/ws/project",
110 "skills": ["read_file", "write_file"],
111 "deliveryKinds": ["webhook"],
112 "sourceRecipeId": "recipe-001",
113 "exitCode": 0,
114 "artifactPath": "/tmp/out.tar.gz",
115 "deliveryStatus": "delivered",
116 "deliveryErrors": []
117 }"#;
118
119 const PRE_HERMES_MINIMAL: &str = r#"{
122 "scheduleId": "sched-min",
123 "epochMinute": 1,
124 "status": "running",
125 "sessionId": "sess-min",
126 "message": ""
127 }"#;
128
129 #[test]
130 fn schedule_run_event_legacy_full_round_trips() {
131 let ev: ScheduleRunEvent =
132 serde_json::from_str(LEGACY_FULL).expect("full legacy record must deserialize");
133 assert_eq!(ev.schema_version, 1);
134 assert_eq!(ev.schedule_id, "sched-abc123");
135 assert_eq!(ev.epoch_minute, 28521600);
136 assert_eq!(ev.status, "completed");
137 assert_eq!(ev.session_id, "sess-xyz789");
138 assert_eq!(ev.started_at, "2026-06-21T10:00:00Z");
139 assert_eq!(ev.finished_at.as_deref(), Some("2026-06-21T10:02:00Z"));
140 assert_eq!(ev.mode, "agent");
141 let limits = ev.resource_limits.as_ref().expect("resourceLimits present");
142 assert_eq!(limits.cpu, Some(2));
143 assert_eq!(limits.memory_mb, Some(4096));
144 assert_eq!(ev.skills, vec!["read_file", "write_file"]);
145 }
146
147 #[test]
148 fn schedule_run_event_pre_hermes_record_uses_defaults() {
149 let ev: ScheduleRunEvent =
150 serde_json::from_str(PRE_HERMES_MINIMAL).expect("pre-hermes record must deserialize");
151 assert_eq!(ev.schema_version, 1);
153 assert_eq!(ev.started_at, "");
154 assert_eq!(ev.finished_at, None);
155 assert_eq!(ev.mode, "agent");
157 assert!(ev.toolset.is_none());
158 assert!(ev.resource_limits.is_none());
159 assert!(ev.skills.is_empty());
160 }
161}