scud/attractor/handlers/
tool.rs1use anyhow::Result;
4use async_trait::async_trait;
5use std::collections::HashMap;
6
7use crate::attractor::context::Context;
8use crate::attractor::graph::{PipelineGraph, PipelineNode};
9use crate::attractor::outcome::Outcome;
10use crate::attractor::run_directory::RunDirectory;
11
12use super::Handler;
13
14pub struct ToolHandler;
15
16#[async_trait]
17impl Handler for ToolHandler {
18 async fn execute(
19 &self,
20 node: &PipelineNode,
21 _context: &Context,
22 _graph: &PipelineGraph,
23 run_dir: &RunDirectory,
24 ) -> Result<Outcome> {
25 let command = node
27 .extra_attrs
28 .get("tool_command")
29 .map(|v| v.as_str())
30 .or_else(|| node.extra_attrs.get("command").map(|v| v.as_str()))
31 .unwrap_or_default();
32
33 if command.is_empty() {
34 return Ok(Outcome::failure("No tool_command attribute specified"));
35 }
36
37 let output = tokio::process::Command::new("sh")
39 .arg("-c")
40 .arg(&command)
41 .output()
42 .await?;
43
44 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
45 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
46
47 let combined = format!("STDOUT:\n{}\nSTDERR:\n{}", stdout, stderr);
49 run_dir.write_response(&node.id, &combined)?;
50
51 let mut updates = HashMap::new();
52 updates.insert(format!("{}.stdout", node.id), serde_json::json!(stdout));
53 updates.insert(
54 format!("{}.exit_code", node.id),
55 serde_json::json!(output.status.code()),
56 );
57
58 if output.status.success() {
59 Ok(Outcome::success()
60 .with_response(stdout)
61 .with_context(updates))
62 } else {
63 Ok(Outcome::failure(format!(
64 "Command failed with exit code {:?}: {}",
65 output.status.code(),
66 stderr.lines().take(5).collect::<Vec<_>>().join("\n")
67 ))
68 .with_context(updates))
69 }
70 }
71}