From 3f04e978c800e66e976f01d3fc614209e6d14b09 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 13 Mar 2026 12:25:37 -0400 Subject: [PATCH] use Freeform question type for human gates with no fixed choices When a human gate has only a freeform edge and no fixed choices, use QuestionType::Freeform instead of MultipleChoice. This sends the user directly to a text input prompt, skipping the unnecessary select menu. Also removes the Quit choice from the REPL workflow so it uses the new freeform-only path. Co-Authored-By: Claude Opus 4.6 --- fabro/workflows/repl/workflow.dot | 13 ++++++ .../fabro-workflows/src/handler/human.rs | 46 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 fabro/workflows/repl/workflow.dot diff --git a/fabro/workflows/repl/workflow.dot b/fabro/workflows/repl/workflow.dot new file mode 100644 index 000000000..787657cb5 --- /dev/null +++ b/fabro/workflows/repl/workflow.dot @@ -0,0 +1,13 @@ +digraph REPL { + graph [goal="Interactive REPL: execute user prompts in a loop"] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + + prompt [shape=hexagon, label="Enter prompt"] + agent [label="Agent", fidelity="summary:high", prompt="Execute the user's request. Their prompt is in the context from the previous human gate."] + + start -> prompt + prompt -> agent [freeform=true] + agent -> prompt +} diff --git a/lib/crates/fabro-workflows/src/handler/human.rs b/lib/crates/fabro-workflows/src/handler/human.rs index 23f013d31..a93e0951d 100644 --- a/lib/crates/fabro-workflows/src/handler/human.rs +++ b/lib/crates/fabro-workflows/src/handler/human.rs @@ -138,7 +138,12 @@ impl Handler for HumanHandler { }) .collect(); - let mut question = Question::new(node.label(), QuestionType::MultipleChoice); + let question_type = if choices.is_empty() { + QuestionType::Freeform + } else { + QuestionType::MultipleChoice + }; + let mut question = Question::new(node.label(), question_type); question.options = options; question.allow_freeform = freeform_target.is_some(); question.stage.clone_from(&node.id); @@ -284,6 +289,7 @@ mod tests { use crate::handler::start::StartHandler; use crate::handler::HandlerRegistry; use crate::interviewer::auto_approve::AutoApproveInterviewer; + use crate::interviewer::recording::RecordingInterviewer; fn make_services() -> EngineServices { EngineServices { @@ -434,4 +440,42 @@ mod tests { Some(&serde_json::json!("custom input")) ); } + + #[tokio::test] + async fn freeform_only_gate_uses_freeform_question_type() { + let inner = Box::new(crate::interviewer::callback::CallbackInterviewer::new( + |_| Answer::text("hello"), + )); + let recorder = Arc::new(RecordingInterviewer::new(inner)); + let handler = HumanHandler::new(recorder.clone()); + + let mut graph = Graph::new("test"); + let mut gate = Node::new("gate"); + gate.attrs.insert( + "label".to_string(), + AttrValue::String("Enter prompt".to_string()), + ); + graph.nodes.insert("gate".to_string(), gate); + graph + .nodes + .insert("target".to_string(), Node::new("target")); + + let mut edge = Edge::new("gate", "target"); + edge.attrs + .insert("freeform".to_string(), AttrValue::Boolean(true)); + graph.edges.push(edge); + + let node = graph.nodes.get("gate").unwrap(); + let context = Context::new(); + let run_dir = Path::new("/tmp/test"); + + handler + .execute(node, &context, &graph, run_dir, &make_services()) + .await + .unwrap(); + + let recordings = recorder.recordings(); + assert_eq!(recordings.len(), 1); + assert_eq!(recordings[0].0.question_type, QuestionType::Freeform); + } }