phi_core/agent_loop/
script_callback.rs1use std::path::{Path, PathBuf};
23use std::process::Command;
24
25#[derive(Debug, Clone)]
27pub struct ScriptCallback {
28 pub path: PathBuf,
30 pub working_dir: Option<PathBuf>,
32}
33
34impl ScriptCallback {
35 pub fn new(path: impl Into<PathBuf>, working_dir: Option<PathBuf>) -> Self {
37 Self {
38 path: path.into(),
39 working_dir,
40 }
41 }
42
43 pub fn execute_sync(
46 &self,
47 input: &serde_json::Value,
48 ) -> Result<serde_json::Value, ScriptCallbackError> {
49 let input_json = serde_json::to_string(input)
50 .map_err(|e| ScriptCallbackError::Serialization(e.to_string()))?;
51
52 let interpreter = detect_interpreter(&self.path);
53 let mut cmd = Command::new(&interpreter[0]);
54 for arg in &interpreter[1..] {
55 cmd.arg(arg);
56 }
57 cmd.arg(&self.path);
58
59 if let Some(ref dir) = self.working_dir {
60 cmd.current_dir(dir);
61 }
62
63 cmd.stdin(std::process::Stdio::piped());
64 cmd.stdout(std::process::Stdio::piped());
65 cmd.stderr(std::process::Stdio::piped());
66
67 let mut child = cmd.spawn().map_err(|e| ScriptCallbackError::Spawn {
68 path: self.path.display().to_string(),
69 error: e.to_string(),
70 })?;
71
72 if let Some(ref mut stdin) = child.stdin.take() {
74 use std::io::Write;
75 let _ = stdin.write_all(input_json.as_bytes());
76 }
77
78 let output = child
79 .wait_with_output()
80 .map_err(|e| ScriptCallbackError::Execution {
81 path: self.path.display().to_string(),
82 error: e.to_string(),
83 })?;
84
85 if !output.status.success() {
86 let stderr = String::from_utf8_lossy(&output.stderr);
87 return Err(ScriptCallbackError::NonZeroExit {
88 path: self.path.display().to_string(),
89 code: output.status.code(),
90 stderr: stderr.to_string(),
91 });
92 }
93
94 let stdout = String::from_utf8_lossy(&output.stdout);
95 serde_json::from_str(stdout.trim())
96 .map_err(|e| ScriptCallbackError::OutputParse(e.to_string()))
97 }
98}
99
100fn detect_interpreter(path: &Path) -> Vec<String> {
102 match path.extension().and_then(|e| e.to_str()) {
103 Some("py") => vec!["python3".into()],
104 Some("sh") => vec!["sh".into()],
105 _ => vec!["sh".into()], }
107}
108
109pub fn is_script_path(s: &str) -> bool {
111 s.ends_with(".sh") || s.ends_with(".py") || s.contains('/')
112}
113
114#[derive(Debug)]
116pub enum ScriptCallbackError {
117 Spawn {
118 path: String,
119 error: String,
120 },
121 Execution {
122 path: String,
123 error: String,
124 },
125 NonZeroExit {
126 path: String,
127 code: Option<i32>,
128 stderr: String,
129 },
130 OutputParse(String),
131 Serialization(String),
132}
133
134impl std::fmt::Display for ScriptCallbackError {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 match self {
137 Self::Spawn { path, error } => write!(f, "Failed to spawn script {path}: {error}"),
138 Self::Execution { path, error } => {
139 write!(f, "Script execution failed {path}: {error}")
140 }
141 Self::NonZeroExit { path, code, stderr } => {
142 write!(f, "Script {path} exited with code {code:?}: {stderr}")
143 }
144 Self::OutputParse(e) => write!(f, "Failed to parse script output as JSON: {e}"),
145 Self::Serialization(e) => write!(f, "Failed to serialize input: {e}"),
146 }
147 }
148}
149
150impl std::error::Error for ScriptCallbackError {}