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 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-13 12:25:37 -04:00
parent 54f3b456f0
commit 3f04e978c8
2 changed files with 58 additions and 1 deletions

View file

@ -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
}

View file

@ -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);
}
}