hive-runtime: drop an ACP refusal on respawn and when the held value is wanted
Choice::refused was cleared only when the agent accepted a session/set_config_option, so it outlived what it described in two cases: a respawned agent process kept the dead one's refusal masking the wanted value in the picker, and a refusal survived a turn whose wanted value was the one the session already held (or none), so re-picking the refused value snapped the picker back to the session's value. start() now resets the refusals along with the offered options, and choose() clears the category's refusal when the wanted value is none or the one the session holds. Refs #4832
This commit is contained in:
parent
25a09dd3ec
commit
7ccbeaa161
1 changed files with 68 additions and 7 deletions
|
|
@ -159,7 +159,8 @@ impl Canceller {
|
|||
pub struct Choice {
|
||||
pub current: String,
|
||||
pub values: Vec<String>,
|
||||
/// The value the agent last refused to set, until it accepts one.
|
||||
/// The value the agent last refused to set, until it accepts one, the
|
||||
/// value wanted is the one the session holds, or the agent is respawned.
|
||||
pub refused: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +212,10 @@ impl Choices {
|
|||
self.lock().options = options;
|
||||
}
|
||||
|
||||
fn reset(&self) {
|
||||
*self.lock() = Offered::default();
|
||||
}
|
||||
|
||||
fn refuse(&self, category: &'static str, value: &str) {
|
||||
self.lock().refused.insert(category, value.to_owned());
|
||||
}
|
||||
|
|
@ -368,7 +373,7 @@ impl<P: CompactionPolicy> AcpRuntime<P> {
|
|||
return Err(AcpError::NoHttpMcp.into());
|
||||
}
|
||||
tracing::info!(agent = %init["agentInfo"], "ACP agent initialized");
|
||||
self.choices.replace(Vec::new());
|
||||
self.choices.reset();
|
||||
Ok(Live {
|
||||
conn,
|
||||
load_session: caps["loadSession"] == Value::Bool(true),
|
||||
|
|
@ -832,8 +837,9 @@ async fn choose_model_and_effort(
|
|||
|
||||
/// Set `session`'s config option of `category` to `wanted`, if the session
|
||||
/// offers that value and holds another. A refusal is reported to `sink` and
|
||||
/// kept as the choice's [`Choice::refused`] until the agent accepts a value;
|
||||
/// the turn goes ahead on the value the session holds.
|
||||
/// kept as the choice's [`Choice::refused`] until the agent accepts a value
|
||||
/// or `wanted` is none or the value the session holds; the turn goes ahead on
|
||||
/// the value the session holds.
|
||||
async fn choose(
|
||||
live: &mut Live,
|
||||
session: &str,
|
||||
|
|
@ -844,9 +850,11 @@ async fn choose(
|
|||
let Some(option) = live.choices.find(category) else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(wanted) =
|
||||
wanted.filter(|w| *w != option.current && option.values.iter().any(|v| v == w))
|
||||
else {
|
||||
if wanted.is_none_or(|w| w == option.current) {
|
||||
live.choices.accept(category);
|
||||
return Ok(());
|
||||
}
|
||||
let Some(wanted) = wanted.filter(|w| option.values.iter().any(|v| v == w)) else {
|
||||
return Ok(());
|
||||
};
|
||||
let params = json!({ "sessionId": session, "configId": option.id, "value": wanted });
|
||||
|
|
@ -1578,4 +1586,57 @@ done
|
|||
Some(choice("m/plain", &["m/think", "m/plain"]))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_refusal_is_dropped_when_the_agent_is_respawned() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let env = [("OPTIONS", "1"), ("REFUSE", "m/plain")];
|
||||
let runtime = agent(dir.path(), "ok", &env, policy(0));
|
||||
let choices = runtime.choices().unwrap();
|
||||
let mut config = Config {
|
||||
model: Some("m/plain".into()),
|
||||
..config(dir.path())
|
||||
};
|
||||
runtime.run(&config, "one", &NoopSink).await.unwrap();
|
||||
assert_eq!(
|
||||
choices.get().model.and_then(|c| c.refused).as_deref(),
|
||||
Some("m/plain")
|
||||
);
|
||||
|
||||
// `haiku` is not on offer, so the new process is asked to set nothing.
|
||||
*runtime.live.lock().await = None;
|
||||
config.model = Some("haiku".into());
|
||||
runtime.run(&config, "two", &NoopSink).await.unwrap();
|
||||
assert_eq!(count(dir.path(), "start"), 2);
|
||||
assert_eq!(sets(dir.path()), ["model=m/plain"]);
|
||||
assert_eq!(
|
||||
choices.get().model,
|
||||
Some(choice("m/think", &["m/think", "m/plain"]))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_refusal_is_dropped_when_the_session_s_own_value_is_wanted() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let env = [("OPTIONS", "1"), ("REFUSE", "m/plain")];
|
||||
let runtime = agent(dir.path(), "ok", &env, policy(0));
|
||||
let choices = runtime.choices().unwrap();
|
||||
let mut config = Config {
|
||||
model: Some("m/plain".into()),
|
||||
..config(dir.path())
|
||||
};
|
||||
runtime.run(&config, "one", &NoopSink).await.unwrap();
|
||||
assert_eq!(
|
||||
choices.get().model.and_then(|c| c.refused).as_deref(),
|
||||
Some("m/plain")
|
||||
);
|
||||
|
||||
config.model = Some("m/think".into());
|
||||
runtime.run(&config, "two", &NoopSink).await.unwrap();
|
||||
assert_eq!(sets(dir.path()), ["model=m/plain"]);
|
||||
assert_eq!(
|
||||
choices.get().model,
|
||||
Some(choice("m/think", &["m/think", "m/plain"]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue