agents: remove the hive-level /effort control too
mara on #5034: "effort shares /models fate". The per-agent web UI no longer sets effort: the `/effort` slash command, the effort picker, `POST /api/effort`, `Bus::set_effort`, the `hyperhive-effort` writer, `EFFORT_LEVELS`/`is_valid_effort` and the ACP effort-list payload are gone, and `lib/modelEffort.ts` with them. The effort chip is display-only and shows its source (`effort_source` in `/api/state`); on ACP it stays hidden while the session offers no effort level. The read side is unchanged: with no swarm value an agent runs an existing `hyperhive-effort` file, else its agent.nix effort, else medium. Existing files are not touched. Refs #5034
This commit is contained in:
parent
594693f4c7
commit
ed271e655f
25 changed files with 170 additions and 464 deletions
|
|
@ -47,7 +47,7 @@ pub(super) async fn api_state(State(state): State<AppState>) -> axum::Json<State
|
|||
let context_window_tokens = state.bus.effective_context_window(&model);
|
||||
let ctx_usage = state.bus.last_ctx_usage();
|
||||
let cost_usage = state.bus.last_cost_usage();
|
||||
let pickers = pickers(&state.bus);
|
||||
let shown = shown(&state.bus);
|
||||
axum::Json(StateSnapshot {
|
||||
seq,
|
||||
label: state.label.clone(),
|
||||
|
|
@ -58,7 +58,7 @@ pub(super) async fn api_state(State(state): State<AppState>) -> axum::Json<State
|
|||
inbox,
|
||||
turn_state,
|
||||
turn_state_since,
|
||||
model: pickers.model,
|
||||
model: shown.model,
|
||||
model_source: state.bus.model_source(),
|
||||
resolved_model,
|
||||
context_window_tokens,
|
||||
|
|
@ -70,8 +70,8 @@ pub(super) async fn api_state(State(state): State<AppState>) -> axum::Json<State
|
|||
.filter(|s| !s.is_empty()),
|
||||
hive_name: crate::identity::hive_name(),
|
||||
swarm_name: crate::identity::swarm_name(),
|
||||
effort: pickers.effort,
|
||||
available_efforts: pickers.available_efforts,
|
||||
effort: shown.effort,
|
||||
effort_source: state.bus.effort_source(),
|
||||
paused: crate::paths::paused_marker().exists(),
|
||||
})
|
||||
}
|
||||
|
|
@ -182,17 +182,12 @@ pub(super) struct StateSnapshot {
|
|||
/// Human name of the swarm (e.g. `"constellat1on"`). Sourced from
|
||||
/// `HYPERHIVE_SWARM_NAME`; `None` when unset.
|
||||
swarm_name: Option<String>,
|
||||
/// Currently-active claude effort level. Reflected on the page so the
|
||||
/// operator's effort picker shows the live selection. Mutable at
|
||||
/// runtime via `POST /api/effort`; applies on the next session.
|
||||
effort: String,
|
||||
/// Selectable effort levels for the picker, ascending. Fixed set
|
||||
/// (`low`, `medium`, `high`, `xhigh`, `max`) — sourced from
|
||||
/// [`crate::harness_state::EFFORT_LEVELS`]. The frontend renders one
|
||||
/// button per entry.
|
||||
/// On ACP, the levels the session offers instead, empty when it offers
|
||||
/// none, which hides the picker (see [`pickers`]).
|
||||
available_efforts: Vec<String>,
|
||||
/// Currently-active claude effort level, shown on the page; `None` per
|
||||
/// [`Shown`]. The swarm sets it (`crate::swarm_inference`); the page
|
||||
/// cannot.
|
||||
effort: Option<String>,
|
||||
/// Where `effort` comes from.
|
||||
effort_source: Source,
|
||||
/// Whether this agent's turn loop is currently parked (the harness
|
||||
/// keeps serving this page + its MCP daemons but drives no turns).
|
||||
/// Same on-disk marker hive-c0re's `Coordinator::is_paused` checks
|
||||
|
|
@ -416,48 +411,36 @@ async fn recent_inbox(socket: &std::path::Path) -> Vec<hive_sh4re::inbox::InboxR
|
|||
}
|
||||
}
|
||||
|
||||
/// The model the page shows, and the effort picker: the value it shows and
|
||||
/// the values it offers.
|
||||
/// The model and effort the page shows. No effort on an ACP agent whose
|
||||
/// session offers no effort level now.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) struct Pickers {
|
||||
pub(super) struct Shown {
|
||||
pub(super) model: String,
|
||||
pub(super) effort: String,
|
||||
pub(super) available_efforts: Vec<String>,
|
||||
pub(super) effort: Option<String>,
|
||||
}
|
||||
|
||||
/// On claude, the requested model and effort and
|
||||
/// [`crate::harness_state::EFFORT_LEVELS`]. On ACP, what the session offers
|
||||
/// (see [`offered_pickers`]).
|
||||
pub(super) fn pickers(bus: &crate::events::Bus) -> Pickers {
|
||||
/// On claude, the requested model and effort. On ACP, see [`offered_shown`].
|
||||
pub(super) fn shown(bus: &crate::events::Bus) -> Shown {
|
||||
let (model, effort) = (bus.model(), bus.effort());
|
||||
match bus.session_choices() {
|
||||
Some(choices) => offered_pickers(model, effort, choices.get()),
|
||||
None => Pickers {
|
||||
Some(choices) => offered_shown(model, effort, choices.get()),
|
||||
None => Shown {
|
||||
model,
|
||||
effort,
|
||||
available_efforts: crate::harness_state::EFFORT_LEVELS
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
effort: Some(effort),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 the model and the
|
||||
/// effort each show 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.
|
||||
fn offered_pickers(
|
||||
model: String,
|
||||
effort: String,
|
||||
offered: hive_runtime::SessionChoices,
|
||||
) -> Pickers {
|
||||
/// What an ACP agent shows. The runtime sets a requested value on the next
|
||||
/// turn, and only if the session offers it, so the model and the effort each
|
||||
/// show 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 no effort shows.
|
||||
fn offered_shown(model: String, effort: String, offered: hive_runtime::SessionChoices) -> Shown {
|
||||
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 {
|
||||
let pick = |wanted: String, choice: Option<&hive_runtime::Choice>| match choice {
|
||||
Some(c) if !settable(&wanted, c) => c.current.clone(),
|
||||
_ => wanted,
|
||||
};
|
||||
|
|
@ -466,10 +449,9 @@ fn offered_pickers(
|
|||
.as_ref()
|
||||
.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()),
|
||||
effort: shown(effort, effort_choice.as_ref()),
|
||||
available_efforts: effort_choice.map(|c| c.values).unwrap_or_default(),
|
||||
Shown {
|
||||
model: pick(model, offered.model.as_ref()),
|
||||
effort: effort_choice.map(|c| pick(effort, Some(&c))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +459,7 @@ fn offered_pickers(
|
|||
mod tests {
|
||||
use hive_runtime::{Choice, SessionChoices};
|
||||
|
||||
use super::{Pickers, offered_pickers};
|
||||
use super::{Shown, offered_shown};
|
||||
|
||||
fn choice(current: &str, values: &[&str]) -> Choice {
|
||||
Choice {
|
||||
|
|
@ -487,51 +469,52 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn strings(values: &[&str]) -> Vec<String> {
|
||||
values.iter().map(|v| (*v).to_owned()).collect()
|
||||
fn shown(model: &str, effort: Option<&str>) -> Shown {
|
||||
Shown {
|
||||
model: model.to_owned(),
|
||||
effort: effort.map(str::to_owned),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_pickers_list_what_the_session_offers() {
|
||||
fn acp_shows_the_requested_values_the_session_offers() {
|
||||
let offered = SessionChoices {
|
||||
model: Some(choice("m/think", &["m/think", "m/plain"])),
|
||||
effort: Some(choice("low", &["low", "high"])),
|
||||
};
|
||||
// `haiku` and `max` are not on offer: the session's own values show.
|
||||
assert_eq!(
|
||||
offered_pickers("haiku".into(), "max".into(), offered.clone()),
|
||||
Pickers {
|
||||
model: "m/think".into(),
|
||||
effort: "low".into(),
|
||||
available_efforts: strings(&["low", "high"]),
|
||||
}
|
||||
offered_shown("haiku".into(), "max".into(), offered.clone()),
|
||||
shown("m/think", Some("low"))
|
||||
);
|
||||
assert_eq!(
|
||||
offered_shown("m/think".into(), "high".into(), offered),
|
||||
shown("m/think", Some("high"))
|
||||
);
|
||||
let picked = offered_pickers("m/think".into(), "high".into(), offered);
|
||||
assert_eq!(picked.effort, "high");
|
||||
assert_eq!(picked.available_efforts, strings(&["low", "high"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_effort_picker_hides_without_effort_or_while_a_model_is_pending() {
|
||||
let plain = SessionChoices {
|
||||
model: Some(choice("m/plain", &["m/think", "m/plain"])),
|
||||
effort: None,
|
||||
};
|
||||
let shown = offered_pickers("m/plain".into(), "high".into(), plain);
|
||||
assert_eq!(shown.model, "m/plain");
|
||||
assert!(shown.available_efforts.is_empty());
|
||||
|
||||
fn acp_shows_no_effort_without_one_on_offer_or_while_a_model_is_pending() {
|
||||
let think = SessionChoices {
|
||||
model: Some(choice("m/think", &["m/think", "m/plain"])),
|
||||
effort: Some(choice("low", &["low", "high"])),
|
||||
};
|
||||
let pending = offered_pickers("m/plain".into(), "high".into(), think);
|
||||
assert_eq!(pending.model, "m/plain");
|
||||
assert!(pending.available_efforts.is_empty());
|
||||
assert_eq!(
|
||||
offered_shown("m/plain".into(), "max".into(), think),
|
||||
shown("m/plain", None)
|
||||
);
|
||||
let plain = SessionChoices {
|
||||
model: Some(choice("m/plain", &["m/think", "m/plain"])),
|
||||
effort: None,
|
||||
};
|
||||
assert_eq!(
|
||||
offered_shown("m/plain".into(), "high".into(), plain),
|
||||
shown("m/plain", None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_pickers_show_the_session_s_model_once_the_agent_refuses_the_wanted_one() {
|
||||
fn acp_shows_the_session_s_model_once_the_agent_refuses_the_wanted_one() {
|
||||
let refused = SessionChoices {
|
||||
model: Some(Choice {
|
||||
refused: Some("m/plain".into()),
|
||||
|
|
@ -540,18 +523,16 @@ mod tests {
|
|||
effort: Some(choice("low", &["low", "high"])),
|
||||
};
|
||||
assert_eq!(
|
||||
offered_pickers("m/plain".into(), "high".into(), refused),
|
||||
Pickers {
|
||||
model: "m/think".into(),
|
||||
effort: "high".into(),
|
||||
available_efforts: strings(&["low", "high"]),
|
||||
}
|
||||
offered_shown("m/plain".into(), "high".into(), refused),
|
||||
shown("m/think", Some("high"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_pickers_are_empty_before_a_session_offers_anything() {
|
||||
let none = offered_pickers("haiku".into(), "high".into(), SessionChoices::default());
|
||||
assert!(none.available_efforts.is_empty());
|
||||
fn acp_shows_the_requested_model_and_no_effort_before_a_session_offers_anything() {
|
||||
assert_eq!(
|
||||
offered_shown("haiku".into(), "high".into(), SessionChoices::default()),
|
||||
shown("haiku", None)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue