diff --git a/crates/xgen-cli/src/composition.rs b/crates/xgen-cli/src/composition.rs index d2fb22f..8a25732 100644 --- a/crates/xgen-cli/src/composition.rs +++ b/crates/xgen-cli/src/composition.rs @@ -141,6 +141,14 @@ pub struct LocalRunRequest { pub final_response_schema: Option, /// Optional exact process checks required before completion. Requires a response schema. pub completion_checks: Option>, + /// Admit `response_candidate` for a goal that plans no steps. + /// + /// Off, a goal needing only an answer closes as + /// `proposal_rejected.completion_without_receipt_completed_plan`: the model + /// has nothing to show a receipt for, and `completion_candidate` requires + /// one. The REPL has always turned this on; `run` could not, so the same + /// question answered interactively failed as a single invocation. + pub conversation_responses: bool, } impl LocalRunRequest { @@ -174,6 +182,7 @@ impl LocalRunRequest { max_model_turns: None, final_response_schema: None, completion_checks: None, + conversation_responses: false, } } } @@ -888,7 +897,8 @@ where F: FnOnce(&str), O: FnMut(DriverProgress) -> DriverProgressControl, { - run_local_composed(request, None, false, None, on_started, on_progress) + let conversational = request.conversation_responses; + run_local_composed(request, None, conversational, None, on_started, on_progress) } /// Start an isolated evaluation Run using the ordinary local tools and driver. @@ -3897,6 +3907,7 @@ mod tests { max_model_turns: None, final_response_schema: None, completion_checks: None, + conversation_responses: false, }; assert_eq!( run_local(request).unwrap(), @@ -3955,6 +3966,7 @@ mod tests { max_model_turns: None, final_response_schema: None, completion_checks: None, + conversation_responses: false, }; assert!(matches!( run_local_with_process_session_progress( diff --git a/crates/xgen-cli/src/main.rs b/crates/xgen-cli/src/main.rs index 4315634..2bfafa0 100644 --- a/crates/xgen-cli/src/main.rs +++ b/crates/xgen-cli/src/main.rs @@ -270,6 +270,13 @@ struct RunArgs { /// Bound work performed by this process invocation. #[arg(long, default_value_t = 32)] max_ticks: u32, + /// Answer a goal that plans no steps, instead of requiring receipt-completed steps. + /// + /// The REPL admits this contract already. Without it a question needing no + /// tool closes as `proposal_rejected.completion_without_receipt_completed_plan`, + /// so a host that drives `run` per message cannot hold a conversation. + #[arg(long)] + conversation_responses: bool, /// Agent loop budget recorded in the Run manifest for workspace discovery runs. #[arg( long, @@ -516,6 +523,9 @@ impl repl::ReplHost for InteractiveHost { max_model_turns: None, final_response_schema: None, completion_checks: None, + // The interactive driver admits this contract regardless; said + // here so the request describes the Run it actually starts. + conversation_responses: true, }, &process_session, |run_id| { @@ -852,6 +862,7 @@ fn run_command(args: RunArgs) -> ExitCode { max_model_turns: args.max_model_turns, final_response_schema, completion_checks, + conversation_responses: args.conversation_responses, }, |run_id| eprintln!("XGEN_STARTED run_id={run_id}"), ))