roder_dynamic_workflows/
store.rs1use std::fs::{self, OpenOptions};
2use std::io::{BufRead, BufReader, Write};
3use std::path::{Path, PathBuf};
4
5use crate::host_api::WorkflowCheckpoint;
6use crate::model::{WorkflowRuntimeError, WorkflowRuntimeErrorKind, WorkflowRuntimeResult};
7use roder_api::subagents::SubagentResult;
8use serde::{Deserialize, Serialize};
9use time::OffsetDateTime;
10
11#[derive(Debug, Clone)]
12pub struct WorkflowCheckpointStore {
13 root: PathBuf,
14}
15
16impl WorkflowCheckpointStore {
17 pub fn new(root: impl Into<PathBuf>) -> Self {
18 Self { root: root.into() }
19 }
20
21 pub fn append_checkpoint(
22 &self,
23 run_id: &str,
24 checkpoint: &WorkflowCheckpoint,
25 ) -> WorkflowRuntimeResult<()> {
26 let dir = self.run_dir(run_id);
27 fs::create_dir_all(&dir).map_err(store_error)?;
28 let path = dir.join("checkpoints.jsonl");
29 let mut file = OpenOptions::new()
30 .create(true)
31 .append(true)
32 .open(path)
33 .map_err(store_error)?;
34 let line = serde_json::to_string(checkpoint).map_err(|err| {
35 WorkflowRuntimeError::new(
36 WorkflowRuntimeErrorKind::Store,
37 format!("checkpoint is not serializable: {err}"),
38 )
39 })?;
40 writeln!(file, "{line}").map_err(store_error)?;
41 Ok(())
42 }
43
44 pub fn read_checkpoints(&self, run_id: &str) -> WorkflowRuntimeResult<Vec<WorkflowCheckpoint>> {
45 let path = self.run_dir(run_id).join("checkpoints.jsonl");
46 if !path.exists() {
47 return Ok(Vec::new());
48 }
49
50 let file = fs::File::open(path).map_err(store_error)?;
51 let reader = BufReader::new(file);
52 let mut checkpoints = Vec::new();
53 for line in reader.lines() {
54 let line = line.map_err(store_error)?;
55 if line.trim().is_empty() {
56 continue;
57 }
58 let checkpoint = serde_json::from_str(&line).map_err(|err| {
59 WorkflowRuntimeError::new(
60 WorkflowRuntimeErrorKind::Store,
61 format!("invalid checkpoint record: {err}"),
62 )
63 })?;
64 checkpoints.push(checkpoint);
65 }
66 Ok(checkpoints)
67 }
68
69 pub fn append_agent_result(
70 &self,
71 run_id: &str,
72 result: &WorkflowCachedAgentResult,
73 ) -> WorkflowRuntimeResult<()> {
74 let dir = self.run_dir(run_id);
75 fs::create_dir_all(&dir).map_err(store_error)?;
76 let path = dir.join("agent-results.jsonl");
77 let mut file = OpenOptions::new()
78 .create(true)
79 .append(true)
80 .open(path)
81 .map_err(store_error)?;
82 let line = serde_json::to_string(result).map_err(|err| {
83 WorkflowRuntimeError::new(
84 WorkflowRuntimeErrorKind::Store,
85 format!("agent result is not serializable: {err}"),
86 )
87 })?;
88 writeln!(file, "{line}").map_err(store_error)?;
89 Ok(())
90 }
91
92 pub fn read_agent_results(
93 &self,
94 run_id: &str,
95 ) -> WorkflowRuntimeResult<Vec<WorkflowCachedAgentResult>> {
96 let path = self.run_dir(run_id).join("agent-results.jsonl");
97 if !path.exists() {
98 return Ok(Vec::new());
99 }
100
101 let file = fs::File::open(path).map_err(store_error)?;
102 let reader = BufReader::new(file);
103 let mut results = Vec::new();
104 for line in reader.lines() {
105 let line = line.map_err(store_error)?;
106 if line.trim().is_empty() {
107 continue;
108 }
109 let result = serde_json::from_str(&line).map_err(|err| {
110 WorkflowRuntimeError::new(
111 WorkflowRuntimeErrorKind::Store,
112 format!("invalid agent result record: {err}"),
113 )
114 })?;
115 results.push(result);
116 }
117 Ok(results)
118 }
119
120 pub fn find_agent_result(
121 &self,
122 run_id: &str,
123 key: &WorkflowAgentCacheKey,
124 ) -> WorkflowRuntimeResult<Option<WorkflowCachedAgentResult>> {
125 Ok(self
126 .read_agent_results(run_id)?
127 .into_iter()
128 .rev()
129 .find(|record| &record.key == key))
130 }
131
132 pub fn invalidate_agent_results(
133 &self,
134 run_id: &str,
135 agent_id: &str,
136 ) -> WorkflowRuntimeResult<usize> {
137 let path = self.run_dir(run_id).join("agent-results.jsonl");
138 if !path.exists() {
139 return Ok(0);
140 }
141
142 let results = self.read_agent_results(run_id)?;
143 let original_len = results.len();
144 let retained = results
145 .into_iter()
146 .filter(|record| record.key.agent_id != agent_id)
147 .collect::<Vec<_>>();
148 let removed = original_len.saturating_sub(retained.len());
149 let mut file = OpenOptions::new()
150 .write(true)
151 .truncate(true)
152 .open(path)
153 .map_err(store_error)?;
154 for result in retained {
155 let line = serde_json::to_string(&result).map_err(|err| {
156 WorkflowRuntimeError::new(
157 WorkflowRuntimeErrorKind::Store,
158 format!("agent result is not serializable: {err}"),
159 )
160 })?;
161 writeln!(file, "{line}").map_err(store_error)?;
162 }
163 Ok(removed)
164 }
165
166 fn run_dir(&self, run_id: &str) -> PathBuf {
167 self.root
168 .join("dynamic-workflows")
169 .join("runs")
170 .join(run_id)
171 }
172
173 pub fn root(&self) -> &Path {
174 &self.root
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
179#[serde(rename_all = "camelCase")]
180pub struct WorkflowAgentCacheKey {
181 pub run_id: String,
182 pub phase_id: String,
183 pub agent_id: String,
184 pub prompt_hash: String,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub model: Option<String>,
187 #[serde(default)]
188 pub tool_scope: Vec<String>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
192#[serde(rename_all = "camelCase")]
193pub struct WorkflowCachedAgentResult {
194 pub key: WorkflowAgentCacheKey,
195 pub result: SubagentResult,
196 #[serde(with = "time::serde::rfc3339")]
197 pub completed_at: OffsetDateTime,
198}
199
200fn store_error(error: std::io::Error) -> WorkflowRuntimeError {
201 WorkflowRuntimeError::new(WorkflowRuntimeErrorKind::Store, error.to_string())
202}