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
100pub fn detect_interpreter(path: &Path) -> Vec<String> {
113 match path.extension().and_then(|e| e.to_str()) {
114 Some("py") => vec!["python3".into()],
115 Some("sh") => vec!["sh".into()],
116 _ => vec!["sh".into()], }
118}
119
120pub fn is_script_path(s: &str) -> bool {
122 s.ends_with(".sh") || s.ends_with(".py") || s.contains('/')
123}
124
125#[derive(Debug)]
127pub enum ScriptCallbackError {
128 Spawn {
129 path: String,
130 error: String,
131 },
132 Execution {
133 path: String,
134 error: String,
135 },
136 NonZeroExit {
137 path: String,
138 code: Option<i32>,
139 stderr: String,
140 },
141 OutputParse(String),
142 Serialization(String),
143}
144
145impl std::fmt::Display for ScriptCallbackError {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 Self::Spawn { path, error } => write!(f, "Failed to spawn script {path}: {error}"),
149 Self::Execution { path, error } => {
150 write!(f, "Script execution failed {path}: {error}")
151 }
152 Self::NonZeroExit { path, code, stderr } => {
153 write!(f, "Script {path} exited with code {code:?}: {stderr}")
154 }
155 Self::OutputParse(e) => write!(f, "Failed to parse script output as JSON: {e}"),
156 Self::Serialization(e) => write!(f, "Failed to serialize input: {e}"),
157 }
158 }
159}
160
161impl std::error::Error for ScriptCallbackError {}