Skip to main content

scud/attractor/handlers/
tool.rs

1//! Tool handler — executes shell commands.
2
3use 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        // Get the tool command from node attributes
26        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        // Execute the command
38        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        // Write output to run directory
48        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}