Skip to main content

schwab_cli/agent/
paths.rs

1use std::path::{Path, PathBuf};
2
3/// Stable filename stem from the rules YAML path (e.g. `options-rules.example`).
4pub 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 backtest_cache_path(rules_path: &Path) -> PathBuf {
45    let dir = rules_runtime_dir(rules_path);
46    let stem = rules_runtime_stem(rules_path);
47    dir.join(format!(".options-backtest-cache-{stem}.json"))
48}
49
50pub fn backtest_state_path(rules_path: &Path) -> PathBuf {
51    let dir = rules_runtime_dir(rules_path);
52    let stem = rules_runtime_stem(rules_path);
53    dir.join(format!("agent-backtest-state-{stem}.json"))
54}
55
56pub fn backtest_journal_path(rules_path: &Path) -> PathBuf {
57    let dir = rules_runtime_dir(rules_path);
58    let stem = rules_runtime_stem(rules_path);
59    dir.join(format!("agent-backtest-journal-{stem}.jsonl"))
60}
61
62pub fn active_state_path(rules_path: &Path, simulate: bool) -> PathBuf {
63    if simulate {
64        sim_state_path(rules_path)
65    } else {
66        default_state_path(rules_path)
67    }
68}
69
70pub fn pid_path(rules_path: &Path) -> PathBuf {
71    let dir = rules_runtime_dir(rules_path);
72    let stem = rules_runtime_stem(rules_path);
73    dir.join(format!("agent-{stem}.pid"))
74}
75
76pub fn log_path(rules_path: &Path) -> PathBuf {
77    let dir = rules_runtime_dir(rules_path);
78    let stem = rules_runtime_stem(rules_path);
79    dir.join(format!("agent-{stem}.log"))
80}
81
82/// Append a line to the per-rules agent log (used by background daemon stdout and watch-mode ticks).
83pub fn append_agent_log(rules_path: &Path, line: &str) -> std::io::Result<()> {
84    use std::io::Write;
85    let log = log_path(rules_path);
86    if let Some(parent) = log.parent() {
87        std::fs::create_dir_all(parent)?;
88    }
89    let mut file = std::fs::OpenOptions::new()
90        .create(true)
91        .append(true)
92        .open(&log)?;
93    writeln!(file, "{line}")?;
94    Ok(())
95}
96
97/// Load persisted state for a rules file, migrating legacy `agent-state.json` when agent_id matches.
98pub fn load_agent_state(rules_path: &Path, agent_id: &str) -> super::state::AgentState {
99    load_agent_state_from(rules_path, agent_id, false)
100}
101
102pub fn load_sim_agent_state(rules_path: &Path, agent_id: &str) -> super::state::AgentState {
103    load_agent_state_from(rules_path, agent_id, true)
104}
105
106fn load_agent_state_from(rules_path: &Path, agent_id: &str, simulate: bool) -> super::state::AgentState {
107    use super::state::{load_state, save_state};
108
109    let state_path = active_state_path(rules_path, simulate);
110    if state_path.exists() {
111        return load_state(&state_path).unwrap_or_default();
112    }
113
114    if simulate {
115        return load_state(&state_path).unwrap_or_default();
116    }
117
118    let legacy = rules_runtime_dir(rules_path).join("agent-state.json");
119    if legacy.exists() {
120        if let Ok(state) = load_state(&legacy) {
121            if state.agent_id.is_empty() || state.agent_id == agent_id {
122                let canonical = default_state_path(rules_path);
123                let _ = save_state(&canonical, &state);
124                return state;
125            }
126        }
127    }
128
129    load_state(&state_path).unwrap_or_default()
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::path::Path;
136
137    #[test]
138    fn runtime_paths_derive_from_rules_filename() {
139        let rules = Path::new("rules/options-rules.example.yaml");
140        assert_eq!(
141            default_state_path(rules),
142            Path::new("rules/agent-state-options-rules.example.json")
143        );
144        assert_eq!(
145            pid_path(rules),
146            Path::new("rules/agent-options-rules.example.pid")
147        );
148        assert_eq!(
149            log_path(rules),
150            Path::new("rules/agent-options-rules.example.log")
151        );
152    }
153
154    #[test]
155    fn distinct_rules_files_get_distinct_state_paths() {
156        let a = default_state_path(Path::new("rules/options-rules.example.yaml"));
157        let b = default_state_path(Path::new("rules/my-options.yaml"));
158        assert_ne!(a, b);
159    }
160}