schwab_cli/agent/
paths.rs1use std::path::{Path, PathBuf};
2
3pub fn rules_runtime_stem(rules_path: &Path) -> String {
5 rules_path
6 .file_stem()
7 .and_then(|s| s.to_str())
8 .filter(|s| !s.is_empty())
9 .unwrap_or("agent")
10 .to_string()
11}
12
13fn rules_runtime_dir(rules_path: &Path) -> PathBuf {
14 rules_path
15 .parent()
16 .map(Path::to_path_buf)
17 .unwrap_or_else(|| PathBuf::from("."))
18}
19
20pub fn default_state_path(rules_path: &Path) -> PathBuf {
21 let dir = rules_runtime_dir(rules_path);
22 let stem = rules_runtime_stem(rules_path);
23 dir.join(format!("agent-state-{stem}.json"))
24}
25
26pub fn sim_state_path(rules_path: &Path) -> PathBuf {
27 let dir = rules_runtime_dir(rules_path);
28 let stem = rules_runtime_stem(rules_path);
29 dir.join(format!("agent-sim-state-{stem}.json"))
30}
31
32pub fn journal_path(rules_path: &Path) -> PathBuf {
33 let dir = rules_runtime_dir(rules_path);
34 let stem = rules_runtime_stem(rules_path);
35 dir.join(format!("agent-journal-{stem}.jsonl"))
36}
37
38pub fn sim_journal_path(rules_path: &Path) -> PathBuf {
39 let dir = rules_runtime_dir(rules_path);
40 let stem = rules_runtime_stem(rules_path);
41 dir.join(format!("agent-sim-journal-{stem}.jsonl"))
42}
43
44pub fn active_state_path(rules_path: &Path, simulate: bool) -> PathBuf {
45 if simulate {
46 sim_state_path(rules_path)
47 } else {
48 default_state_path(rules_path)
49 }
50}
51
52pub fn pid_path(rules_path: &Path) -> PathBuf {
53 let dir = rules_runtime_dir(rules_path);
54 let stem = rules_runtime_stem(rules_path);
55 dir.join(format!("agent-{stem}.pid"))
56}
57
58pub fn log_path(rules_path: &Path) -> PathBuf {
59 let dir = rules_runtime_dir(rules_path);
60 let stem = rules_runtime_stem(rules_path);
61 dir.join(format!("agent-{stem}.log"))
62}
63
64pub fn append_agent_log(rules_path: &Path, line: &str) -> std::io::Result<()> {
66 use std::io::Write;
67 let log = log_path(rules_path);
68 if let Some(parent) = log.parent() {
69 std::fs::create_dir_all(parent)?;
70 }
71 let mut file = std::fs::OpenOptions::new()
72 .create(true)
73 .append(true)
74 .open(&log)?;
75 writeln!(file, "{line}")?;
76 Ok(())
77}
78
79pub fn load_agent_state(rules_path: &Path, agent_id: &str) -> super::state::AgentState {
81 load_agent_state_from(rules_path, agent_id, false)
82}
83
84pub fn load_sim_agent_state(rules_path: &Path, agent_id: &str) -> super::state::AgentState {
85 load_agent_state_from(rules_path, agent_id, true)
86}
87
88fn load_agent_state_from(rules_path: &Path, agent_id: &str, simulate: bool) -> super::state::AgentState {
89 use super::state::{load_state, save_state};
90
91 let state_path = active_state_path(rules_path, simulate);
92 if state_path.exists() {
93 return load_state(&state_path).unwrap_or_default();
94 }
95
96 if simulate {
97 return load_state(&state_path).unwrap_or_default();
98 }
99
100 let legacy = rules_runtime_dir(rules_path).join("agent-state.json");
101 if legacy.exists() {
102 if let Ok(state) = load_state(&legacy) {
103 if state.agent_id.is_empty() || state.agent_id == agent_id {
104 let canonical = default_state_path(rules_path);
105 let _ = save_state(&canonical, &state);
106 return state;
107 }
108 }
109 }
110
111 load_state(&state_path).unwrap_or_default()
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use std::path::Path;
118
119 #[test]
120 fn runtime_paths_derive_from_rules_filename() {
121 let rules = Path::new("rules/options-rules.example.yaml");
122 assert_eq!(
123 default_state_path(rules),
124 Path::new("rules/agent-state-options-rules.example.json")
125 );
126 assert_eq!(
127 pid_path(rules),
128 Path::new("rules/agent-options-rules.example.pid")
129 );
130 assert_eq!(
131 log_path(rules),
132 Path::new("rules/agent-options-rules.example.log")
133 );
134 }
135
136 #[test]
137 fn distinct_rules_files_get_distinct_state_paths() {
138 let a = default_state_path(Path::new("rules/options-rules.example.yaml"));
139 let b = default_state_path(Path::new("rules/my-options.yaml"));
140 assert_ne!(a, b);
141 }
142}