hive-runtime, hive-agent: model/effort picker for ACP agents from configOptions
An ACP agent's model and effort pickers now list what its session offers (its `model` and `thought_level` config options) instead of the claude model list and EFFORT_LEVELS. A pick goes through the same Bus::set_model / Bus::set_effort -> Config.model / Config.effort path as on claude; before each prompt the ACP runtime sets it with `session/set_config_option`, model first, and only when the session offers that value. Options are re-read from the set response and from `config_option_update`, so the effort picker disappears when the chosen model offers no effort levels, and is hidden while a newly picked model waits for the next turn. The session's options reach the web UI through a `Choices` handle from a new `Runtime::choices`, registered on the bus the way `canceller` is. /api/model and /api/effort accept only offered values on ACP. The claude path is unchanged. Refs #4391
This commit is contained in:
parent
abd547f600
commit
f189724a4c
13 changed files with 559 additions and 44 deletions
|
|
@ -212,6 +212,61 @@ pub(super) fn advertised_commands(update: &Value) -> Option<Vec<String>> {
|
|||
)
|
||||
}
|
||||
|
||||
/// One select config option a session advertises.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct ConfigOption {
|
||||
pub(super) id: String,
|
||||
pub(super) category: Option<String>,
|
||||
pub(super) current: String,
|
||||
pub(super) values: Vec<String>,
|
||||
}
|
||||
|
||||
/// The select options in `value`'s `configOptions`, as a session response, a
|
||||
/// `session/set_config_option` response and a `config_option_update` carry
|
||||
/// them; `None` if it has none. Grouped values are flattened; boolean options
|
||||
/// are left out.
|
||||
pub(super) fn config_options(value: &Value) -> Option<Vec<ConfigOption>> {
|
||||
let options = value.get("configOptions")?.as_array()?;
|
||||
Some(
|
||||
options
|
||||
.iter()
|
||||
.filter_map(|option| {
|
||||
let entries = option.get("options").and_then(Value::as_array);
|
||||
let values = entries
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(
|
||||
|entry| match entry.get("options").and_then(Value::as_array) {
|
||||
Some(group) => group.iter().collect(),
|
||||
None => vec![entry],
|
||||
},
|
||||
)
|
||||
.filter_map(|entry| entry.get("value").and_then(Value::as_str))
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
Some(ConfigOption {
|
||||
id: option.get("id")?.as_str()?.to_owned(),
|
||||
category: option
|
||||
.get("category")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned),
|
||||
current: option.get("currentValue")?.as_str()?.to_owned(),
|
||||
values,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The options a `config_option_update` sets, or `None` for any other
|
||||
/// `update`. Each replaces the session's previous options.
|
||||
pub(super) fn updated_config_options(update: &Value) -> Option<Vec<ConfigOption>> {
|
||||
if update.get("sessionUpdate").and_then(Value::as_str) != Some("config_option_update") {
|
||||
return None;
|
||||
}
|
||||
Some(config_options(update).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Convert a claude `--mcp-config` document into ACP's `mcpServers` list.
|
||||
/// Returns the list and the server names, in the same order.
|
||||
pub(super) fn mcp_servers(config: &Value) -> (Vec<Value>, Vec<String>) {
|
||||
|
|
@ -346,7 +401,9 @@ fn tool_result(id: &str, output: &str, is_error: bool) -> Value {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{StreamMapper, canonical_tool_name, mcp_servers, session_model};
|
||||
use super::{
|
||||
ConfigOption, StreamMapper, canonical_tool_name, config_options, mcp_servers, session_model,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn mapper() -> StreamMapper {
|
||||
|
|
@ -547,4 +604,31 @@ mod tests {
|
|||
assert_eq!(session_model(&resp).as_deref(), Some("p/m-2"));
|
||||
assert_eq!(session_model(&json!({})), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_options_flatten_groups_and_skip_booleans() {
|
||||
let resp = json!({
|
||||
"configOptions": [
|
||||
{ "id": "model", "name": "Model", "category": "model", "type": "select",
|
||||
"currentValue": "p/a",
|
||||
"options": [
|
||||
{ "group": "p", "name": "P", "options": [
|
||||
{ "value": "p/a", "name": "A" }, { "value": "p/b", "name": "B" },
|
||||
] },
|
||||
{ "value": "q/c", "name": "C" },
|
||||
] },
|
||||
{ "id": "fast", "name": "Fast", "type": "boolean", "currentValue": true },
|
||||
],
|
||||
});
|
||||
assert_eq!(
|
||||
config_options(&resp),
|
||||
Some(vec![ConfigOption {
|
||||
id: "model".into(),
|
||||
category: Some("model".into()),
|
||||
current: "p/a".into(),
|
||||
values: vec!["p/a".into(), "p/b".into(), "q/c".into()],
|
||||
}])
|
||||
);
|
||||
assert_eq!(config_options(&json!({})), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue