agent_engine/session/
client_args.rs1use std::path::{Path, PathBuf};
28
29use super::types::SessionConfig;
30
31pub fn looks_path_like(val: &str) -> bool {
39 if val.contains('\n') || val.contains(" ") {
41 return false;
42 }
43 let trimmed = val.trim();
44 if trimmed.is_empty() {
45 return false;
46 }
47
48 let starts = trimmed.starts_with('/')
49 || trimmed.starts_with("./")
50 || trimmed.starts_with("../")
51 || trimmed.starts_with('~');
52
53 let lower = trimmed.to_ascii_lowercase();
54 let ends = lower.ends_with(".md") || lower.ends_with(".txt");
55
56 starts || ends
57}
58
59fn resolve_against(val: &str, client_cwd: &Path) -> PathBuf {
62 let trimmed = val.trim();
63 if let Some(rest) = trimmed.strip_prefix('~') {
64 if let Some(home) = dirs::home_dir() {
65 return home.join(rest.strip_prefix('/').unwrap_or(rest));
66 }
67 }
68 let p = Path::new(trimmed);
69 if p.is_absolute() {
70 p.to_path_buf()
71 } else {
72 client_cwd.join(p)
73 }
74}
75
76pub fn resolve_client_session_args(
85 cfg: &mut SessionConfig,
86 client_cwd: &Path,
87) -> Result<(), String> {
88 if let Some(ref val) = cfg.system {
90 if looks_path_like(val) {
91 let abs = resolve_against(val, client_cwd);
92 match std::fs::read_to_string(&abs) {
93 Ok(contents) => {
94 cfg.system = Some(contents);
95 }
96 Err(_) => {
97 return Err(format!(
98 "--system {val}: no such file (resolved {})",
99 abs.display()
100 ));
101 }
102 }
103 }
104 }
105
106 if let Some(ref val) = cfg.prompt_manifest {
108 let abs = if val.is_absolute() {
109 val.clone()
110 } else {
111 client_cwd.join(val)
112 };
113 if !abs.exists() {
114 return Err(format!(
115 "--prompt-manifest {}: no such file (resolved {})",
116 val.display(),
117 abs.display()
118 ));
119 }
120 cfg.prompt_manifest = Some(abs);
121 }
122
123 Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn path_like_detection() {
132 assert!(looks_path_like("./prompt.md"));
134 assert!(looks_path_like("../prompt.md"));
135 assert!(looks_path_like("/tmp/prompt.md"));
136 assert!(looks_path_like("~/prompts/system.md"));
137 assert!(looks_path_like("notes.txt"));
138 assert!(looks_path_like("NOTES.MD"));
139 assert!(looks_path_like("./foo"));
140
141 assert!(!looks_path_like("You are a helpful assistant"));
143 assert!(!looks_path_like("Be concise and direct. Use tools."));
144 assert!(!looks_path_like("multi\nline\nprompt"));
145 assert!(!looks_path_like("has double spaces"));
146 assert!(!looks_path_like(""));
147 }
148
149 #[test]
150 fn relative_resolution_against_cwd() {
151 let cwd = Path::new("/tmp/proj");
152 let abs = resolve_against("./prompt.md", cwd);
153 assert_eq!(abs, PathBuf::from("/tmp/proj/prompt.md"));
154 let abs = resolve_against("../other/p.md", cwd);
155 assert_eq!(abs, PathBuf::from("/tmp/proj/../other/p.md"));
156 }
157
158 #[test]
159 fn absolute_path_unchanged() {
160 let cwd = Path::new("/tmp/proj");
161 let abs = resolve_against("/etc/prompt.md", cwd);
162 assert_eq!(abs, PathBuf::from("/etc/prompt.md"));
163 }
164
165 #[test]
166 fn system_content_replacement() {
167 let dir = tempfile::tempdir().unwrap();
168 let file = dir.path().join("p.md");
169 std::fs::write(&file, "You are PROOF-T4").unwrap();
170
171 let mut cfg = SessionConfig {
172 system: Some("./p.md".into()),
173 ..Default::default()
174 };
175 resolve_client_session_args(&mut cfg, dir.path()).unwrap();
176 assert_eq!(cfg.system.as_deref(), Some("You are PROOF-T4"));
177 }
178
179 #[test]
180 fn system_missing_file_errors() {
181 let dir = tempfile::tempdir().unwrap();
182 let mut cfg = SessionConfig {
183 system: Some("./missing.md".into()),
184 ..Default::default()
185 };
186 let err = resolve_client_session_args(&mut cfg, dir.path()).unwrap_err();
187 assert!(err.contains("--system ./missing.md"), "err={err}");
188 assert!(err.contains("no such file"), "err={err}");
189 assert!(
191 err.contains("missing.md"),
192 "err={err}"
193 );
194 }
195
196 #[test]
197 fn system_prose_passes_through() {
198 let dir = tempfile::tempdir().unwrap();
199 let mut cfg = SessionConfig {
200 system: Some("You are a helpful assistant".into()),
201 ..Default::default()
202 };
203 resolve_client_session_args(&mut cfg, dir.path()).unwrap();
204 assert_eq!(
205 cfg.system.as_deref(),
206 Some("You are a helpful assistant")
207 );
208 }
209
210 #[test]
211 fn prompt_manifest_canonicalized() {
212 let dir = tempfile::tempdir().unwrap();
213 let file = dir.path().join("manifest.toml");
214 std::fs::write(&file, "[prompt]").unwrap();
215
216 let mut cfg = SessionConfig {
217 prompt_manifest: Some(PathBuf::from("manifest.toml")),
218 ..Default::default()
219 };
220 resolve_client_session_args(&mut cfg, dir.path()).unwrap();
221 assert!(cfg.prompt_manifest.as_ref().unwrap().is_absolute());
222 }
223
224 #[test]
225 fn prompt_manifest_missing_errors() {
226 let dir = tempfile::tempdir().unwrap();
227 let mut cfg = SessionConfig {
228 prompt_manifest: Some(PathBuf::from("nope.toml")),
229 ..Default::default()
230 };
231 let err = resolve_client_session_args(&mut cfg, dir.path()).unwrap_err();
232 assert!(err.contains("--prompt-manifest"), "err={err}");
233 assert!(err.contains("no such file"), "err={err}");
234 }
235}