cascade_agent/skills/
executor.rs1use std::path::Path;
4use std::time::Duration;
5
6use tokio::io::AsyncWriteExt;
7use tokio::process::Command;
8use tracing::{debug, error, warn};
9
10use crate::tools::ToolResult;
11
12pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
14
15pub struct SkillExecutor;
25
26impl SkillExecutor {
27 pub async fn execute(
33 executable: &Path,
34 args: serde_json::Value,
35 skill_dir: &Path,
36 timeout: Duration,
37 ) -> ToolResult {
38 debug!(
39 exe = %executable.display(),
40 skill_dir = %skill_dir.display(),
41 "Spawning skill executable"
42 );
43
44 let mut child = match Command::new(executable)
45 .current_dir(skill_dir)
46 .stdin(std::process::Stdio::piped())
47 .stdout(std::process::Stdio::piped())
48 .stderr(std::process::Stdio::piped())
49 .spawn()
50 {
51 Ok(c) => c,
52 Err(e) => {
53 error!(exe = %executable.display(), "Failed to spawn: {e}");
54 return ToolResult::err(format!(
55 "Failed to spawn skill executable '{}': {}",
56 executable.display(),
57 e
58 ));
59 }
60 };
61
62 if let Some(mut stdin) = child.stdin.take() {
64 let json_bytes = match serde_json::to_vec(&args) {
65 Ok(b) => b,
66 Err(e) => {
67 return ToolResult::err(format!("Failed to serialise skill arguments: {}", e));
68 }
69 };
70
71 if let Err(e) = stdin.write_all(&json_bytes).await {
72 return ToolResult::err(format!("Failed to write to skill stdin: {}", e));
73 }
74 if let Err(e) = stdin.shutdown().await {
75 warn!("Failed to close skill stdin: {e}");
76 }
77 }
78
79 let result = match tokio::time::timeout(timeout, child.wait_with_output()).await {
81 Ok(Ok(output)) => output,
82 Ok(Err(e)) => {
83 error!(exe = %executable.display(), "Process error: {e}");
84 return ToolResult::err(format!("Skill process error: {}", e));
85 }
86 Err(_elapsed) => {
87 return ToolResult::err(format!(
90 "Skill '{}' timed out after {:?}",
91 executable.display(),
92 timeout
93 ));
94 }
95 };
96
97 if !result.stderr.is_empty() {
99 let stderr_str = String::from_utf8_lossy(&result.stderr);
100 if !result.status.success() {
101 warn!(
102 skill = %executable.display(),
103 "stderr (exit {:?}): {}",
104 result.status.code(),
105 stderr_str
106 );
107 } else {
108 debug!(
109 skill = %executable.display(),
110 "stderr: {}",
111 stderr_str
112 );
113 }
114 }
115
116 let stdout_str = String::from_utf8_lossy(&result.stdout);
118
119 if stdout_str.trim().is_empty() {
120 return if result.status.success() {
121 ToolResult::ok(serde_json::Value::Null)
122 } else {
123 ToolResult::err(format!(
124 "Skill exited with {:?} and produced no output",
125 result.status.code()
126 ))
127 };
128 }
129
130 match serde_json::from_str::<ToolResult>(&stdout_str) {
131 Ok(tool_result) => tool_result,
132 Err(e) => {
133 warn!(
134 skill = %executable.display(),
135 "Stdout is not valid ToolResult JSON: {e}. Raw stdout: {stdout_str}"
136 );
137 ToolResult::ok(serde_json::json!({
139 "raw_output": stdout_str.trim().to_string(),
140 "parse_warning": "Executable output was not valid ToolResult JSON"
141 }))
142 }
143 }
144 }
145}