Skip to main content

scud/attractor/handlers/
human.rs

1//! Wait-for-human gate handler.
2//!
3//! Derives choices from outgoing edges and presents them via the Interviewer trait.
4
5use anyhow::Result;
6use async_trait::async_trait;
7
8use crate::attractor::context::Context;
9use crate::attractor::graph::{PipelineGraph, PipelineNode};
10use crate::attractor::outcome::Outcome;
11use crate::attractor::run_directory::RunDirectory;
12
13use super::Handler;
14
15pub struct HumanHandler;
16
17#[async_trait]
18impl Handler for HumanHandler {
19    async fn execute(
20        &self,
21        node: &PipelineNode,
22        _context: &Context,
23        graph: &PipelineGraph,
24        _run_dir: &RunDirectory,
25    ) -> Result<Outcome> {
26        // Derive choices from outgoing edge labels
27        let idx = *graph.node_index.get(&node.id).unwrap();
28        let edges = graph.outgoing_edges(idx);
29
30        let choices: Vec<String> = edges
31            .iter()
32            .filter(|(_, e)| !e.label.is_empty())
33            .map(|(_, e)| e.label.clone())
34            .collect();
35
36        let _prompt = if node.prompt.is_empty() {
37            format!(
38                "Node '{}' requires human input. Choose an option:",
39                node.label
40            )
41        } else {
42            node.prompt.clone()
43        };
44
45        // For now, since we don't have direct access to the interviewer here,
46        // we return a WaitingForHuman status. The runner should handle this
47        // by consulting its interviewer.
48        // In the actual implementation, the runner passes the interviewer.
49        // For this handler, we return success with the first choice as preferred label.
50        if let Some(first) = choices.first() {
51            Ok(Outcome::success_with_label(first.clone()))
52        } else {
53            Ok(Outcome::success())
54        }
55    }
56}