Watch
0
0
Fork
You've already forked hyperhive
0

hive-runtime, hive-agent: clear the ACP picker's pending model on a refusal

When an ACP agent answered session/set_config_option with an error,
choose() only logged it, so the session's current value never moved and
offered_pickers() kept treating the refused model as pending: the picker
showed the refused model as current and hid the effort picker.

hive-runtime now keeps the refused value per category on Choices, exposed
as Choice::refused, set on the RPC error and cleared when the agent next
accepts a value for that category. offered_pickers() treats a refused
value as not settable, so the picker shows the session's actual model and
its effort levels.

The fake ACP agent gains REFUSE, which errors the first
session/set_config_option to that value.

Refs #4832 (ACP half only)
This commit is contained in:
atlas 2026-09-30 12:47:03 +02:00 • committed by mara
commit 25a09dd3ec
2 changed files with 110 additions and 19 deletions

View file

@ -538,9 +538,9 @@ pub(super) fn pickers(bus: &crate::events::Bus) -> Pickers {
/// Pickers for what an ACP session offers. The runtime sets a requested value
/// on the next turn, and only if the session offers it, so each picker shows
/// the requested value when offered and the session's own otherwise. The
/// effort levels on offer come with the model, so while a new model waits
/// for the next turn the effort picker is empty. `configured` narrows the
/// the requested value when offered and not refused by the agent, and the
/// session's own otherwise. The effort levels on offer come with the model,
/// so while a new model waits for the next turn the effort picker is empty. `configured` narrows the
/// model list per [`filter_offered_models`]; it never narrows which value is
/// shown as current, only which values the picker offers alongside it.
fn offered_pickers(
@ -549,14 +549,17 @@ fn offered_pickers(
offered: hive_runtime::SessionChoices,
configured: Option<&[String]>,
) -> Pickers {
let settable = |wanted: &String, c: &hive_runtime::Choice| {
c.values.contains(wanted) && c.refused.as_ref() != Some(wanted)
};
let shown = |wanted: String, choice: Option<&hive_runtime::Choice>| match choice {
Some(c) if !c.values.contains(&wanted) => c.current.clone(),
Some(c) if !settable(&wanted, c) => c.current.clone(),
_ => wanted,
};
let model_pending = offered
.model
.as_ref()
.is_some_and(|c| c.current != model && c.values.contains(&model));
.is_some_and(|c| c.current != model && settable(&model, c));
let effort_choice = offered.effort.filter(|_| !model_pending);
Pickers {
model: shown(model, offered.model.as_ref()),
@ -581,6 +584,7 @@ mod tests {
Choice {
current: current.to_owned(),
values: values.iter().map(|v| (*v).to_owned()).collect(),
refused: None,
}
}
@ -628,6 +632,26 @@ mod tests {
assert!(pending.available_efforts.is_empty());
}
#[test]
fn acp_pickers_show_the_session_s_model_once_the_agent_refuses_the_wanted_one() {
let refused = SessionChoices {
model: Some(Choice {
refused: Some("m/plain".into()),
..choice("m/think", &["m/think", "m/plain"])
}),
effort: Some(choice("low", &["low", "high"])),
};
assert_eq!(
offered_pickers("m/plain".into(), "high".into(), refused, None),
Pickers {
model: "m/think".into(),
available_models: strings(&["m/think", "m/plain"]),
effort: "high".into(),
available_efforts: strings(&["low", "high"]),
}
);
}
#[test]
fn acp_pickers_are_empty_before_a_session_offers_anything() {
let none = offered_pickers(