remove c0re.operatorPronouns and c0re.contextWindowTokens
Both options are now ignored with a warning (the swarm-peers-removed.nix shape), so a host that still sets one keeps evaluating. - operatorPronouns: the agents' system prompt names the pronouns as she/her, the value the harness fell back to and the option's default. The --operator-pronouns flag, HiveEnv field, meta-flake let binding and HIVE_OPERATOR_PRONOUNS env var are gone. - contextWindowTokens: the per-family defaults (haiku 200000, sonnet 1000000, opus 1000000) move into hive-agent's harness_state as a table; the window the API reports after a turn still wins. The --context-window-tokens flag, HiveEnv field and the rendered HIVE_CONTEXT_WINDOW_TOKENS_<KEY> env vars are gone.
This commit is contained in:
parent
5cd1f16405
commit
4b5707768c
18 changed files with 151 additions and 258 deletions
|
|
@ -1,4 +1,4 @@
|
|||
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. The operator (the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person (e.g. when relaying to a peer or the manager). When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`) so the operator + peers can disambiguate; within your own hive the short form is fine.
|
||||
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. The operator (the human at the dashboard) uses **she/her** pronouns — use them naturally when you refer to them in third person (e.g. when relaying to a peer or the manager). When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`) so the operator + peers can disambiguate; within your own hive the short form is fine.
|
||||
|
||||
Tools (hyperhive surface). Full signature + behavior for each comes from the tool's own MCP description (you already received it via the MCP tool schema) — this is just the map of what exists and which ones are gated, so you know where to look:
|
||||
|
||||
|
|
|
|||
|
|
@ -367,10 +367,10 @@ pub struct Bus {
|
|||
/// Per-inference context-window size as reported by the Anthropic API
|
||||
/// in the stream-json `result` event (`modelUsage.*.contextWindow`).
|
||||
/// Set by the stdout pump on every completed turn. Takes precedence
|
||||
/// over the Nix-configured `HIVE_CONTEXT_WINDOW_TOKENS_*` env vars
|
||||
/// over the per-model defaults in `harness_state::context_window_tokens`
|
||||
/// for compaction watermark calculations — it reflects the actual
|
||||
/// limit the model enforces, which may differ from what the operator
|
||||
/// configured (e.g. 200 k active window on a 1 M cache-enabled model).
|
||||
/// limit the model enforces, which may differ from the default
|
||||
/// (e.g. 200 k active window on a 1 M cache-enabled model).
|
||||
api_context_window: Arc<Mutex<Option<u64>>>,
|
||||
/// Resolved model id from the most recent turn's `assistant` events
|
||||
/// (the API-echoed `message.model`, e.g. `claude-opus-4-8`), as
|
||||
|
|
|
|||
|
|
@ -237,33 +237,31 @@ pub fn configured_effort() -> Option<&'static str> {
|
|||
.map(|s| &*Box::leak(s.into_boxed_str()))
|
||||
}
|
||||
|
||||
/// Context-window size in tokens for a given model name.
|
||||
///
|
||||
/// Canonical per-model sizes are declared in the harness nix modules as
|
||||
/// `hyperhive.contextWindowTokens` and injected as
|
||||
/// `HIVE_CONTEXT_WINDOW_TOKENS_<KEY_UPPER>` env vars — so this function
|
||||
/// normally just reads them. The Rust code carries no model knowledge;
|
||||
/// updating model families only requires a Nix change.
|
||||
/// Context-window size in tokens per model family, keyed by a substring
|
||||
/// of the model name (`"sonnet"` matches `"claude-sonnet-4-5"`).
|
||||
const CONTEXT_WINDOW_DEFAULTS: [(&str, u64); 3] = [
|
||||
("haiku", 200_000),
|
||||
("sonnet", 1_000_000),
|
||||
("opus", 1_000_000),
|
||||
];
|
||||
|
||||
/// Context-window size in tokens for a given model name, used until a
|
||||
/// completed turn reports the real window (`Bus::effective_context_window`
|
||||
/// prefers that one).
|
||||
///
|
||||
/// Resolution order (first match wins):
|
||||
/// 1. `HIVE_CONTEXT_WINDOW_TOKENS_<KEY>` — key (lowercased) is a
|
||||
/// substring of the active model name. Populated by the Nix default
|
||||
/// map for all known families; add/override in `agent.nix`.
|
||||
/// 1. [`CONTEXT_WINDOW_DEFAULTS`] — the family key, case-insensitively,
|
||||
/// is a substring of the model name.
|
||||
/// 2. `HIVE_CONTEXT_WINDOW_TOKENS` — single global override (any model).
|
||||
/// 3. Hard fallback: `200_000` (conservative; only hit outside NixOS).
|
||||
/// 3. Hard fallback: `200_000`.
|
||||
#[must_use]
|
||||
pub fn context_window_tokens(model: &str) -> u64 {
|
||||
let m = model.to_ascii_lowercase();
|
||||
// Per-model env vars set by `hyperhive.contextWindowTokens` in Nix.
|
||||
for (key, val) in std::env::vars() {
|
||||
if let Some(suffix) = key.strip_prefix("HIVE_CONTEXT_WINDOW_TOKENS_")
|
||||
&& !suffix.is_empty()
|
||||
&& m.contains(&suffix.to_ascii_lowercase())
|
||||
&& let Ok(v) = val.trim().parse::<u64>()
|
||||
&& v > 0
|
||||
{
|
||||
return v;
|
||||
}
|
||||
if let Some((_, tokens)) = CONTEXT_WINDOW_DEFAULTS
|
||||
.iter()
|
||||
.find(|(family, _)| m.contains(family))
|
||||
{
|
||||
return *tokens;
|
||||
}
|
||||
// Global override (single value, any model).
|
||||
if let Ok(s) = std::env::var("HIVE_CONTEXT_WINDOW_TOKENS")
|
||||
|
|
@ -272,13 +270,25 @@ pub fn context_window_tokens(model: &str) -> u64 {
|
|||
{
|
||||
return v;
|
||||
}
|
||||
// Hard fallback for dev/test outside NixOS where env vars aren't set.
|
||||
200_000
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DEFAULT_EFFORT, DEFAULT_MODEL, Source, effort_from, model_from};
|
||||
use super::{
|
||||
DEFAULT_EFFORT, DEFAULT_MODEL, Source, context_window_tokens, effort_from, model_from,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn context_window_defaults_per_model_family() {
|
||||
assert_eq!(context_window_tokens("claude-haiku-4-5"), 200_000);
|
||||
assert_eq!(context_window_tokens("haiku"), 200_000);
|
||||
assert_eq!(context_window_tokens("claude-sonnet-4-5"), 1_000_000);
|
||||
assert_eq!(context_window_tokens("Claude-Sonnet-4-6"), 1_000_000);
|
||||
assert_eq!(context_window_tokens("sonnet"), 1_000_000);
|
||||
assert_eq!(context_window_tokens("claude-opus-4-1"), 1_000_000);
|
||||
assert_eq!(context_window_tokens("opus"), 1_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiled_default_effort_matches_claude_settings() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
/// Assemble the system prompt for a given label + pronouns + optional hive /
|
||||
/// Assemble the system prompt for a given label + optional hive /
|
||||
/// swarm display names. Pure function — no I/O. Splits out from
|
||||
/// [`write_system_prompt`] so the marker logic + substitution is unit-testable
|
||||
/// in isolation. The caller supplies the template body so tests can pass an
|
||||
|
|
@ -21,7 +21,6 @@ use anyhow::{Context, Result};
|
|||
pub fn render(
|
||||
template: &str,
|
||||
label: &str,
|
||||
operator_pronouns: &str,
|
||||
hive_name: Option<&str>,
|
||||
swarm_name: Option<&str>,
|
||||
docs_dir: Option<&str>,
|
||||
|
|
@ -37,7 +36,6 @@ pub fn render(
|
|||
let rendered = body
|
||||
.replace("{label}", label)
|
||||
.replace("{qualified_label}", &qualified)
|
||||
.replace("{operator_pronouns}", operator_pronouns)
|
||||
.replace("{hive_identity}", &hive_identity)
|
||||
.replace("{swarm_identity}", &swarm_identity);
|
||||
// When the reference docs are mounted in-container (`services.hyperhive.agent.docs.enable`
|
||||
|
|
@ -114,7 +112,6 @@ fn parse_close_marker(line: &str) -> Option<&str> {
|
|||
pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf> {
|
||||
let parent = crate::paths::config_dir();
|
||||
tokio::fs::create_dir_all(&parent).await.ok();
|
||||
let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
|
||||
let template_path = hive_sh4re::assets::prompt_template();
|
||||
let template = tokio::fs::read_to_string(&template_path)
|
||||
.await
|
||||
|
|
@ -137,7 +134,6 @@ pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf>
|
|||
let body = render(
|
||||
&template,
|
||||
label,
|
||||
&pronouns,
|
||||
hive_name.as_deref(),
|
||||
swarm_name.as_deref(),
|
||||
docs_dir.as_deref(),
|
||||
|
|
@ -236,13 +232,11 @@ shared closer
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn render_substitutes_label_and_pronouns() {
|
||||
// Real template's first agent line — keeps the renderer
|
||||
// honest about the {label} / {operator_pronouns} pair the
|
||||
// harness already relied on.
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "they/them", None, None, None);
|
||||
fn render_substitutes_label_and_keeps_the_pronoun_line() {
|
||||
// Real template's first agent line.
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None);
|
||||
assert!(rendered.contains("hyperhive agent `alice`"));
|
||||
assert!(rendered.contains("**they/them** pronouns"));
|
||||
assert!(rendered.contains("uses **she/her** pronouns"));
|
||||
assert!(!rendered.contains("{label}"));
|
||||
assert!(!rendered.contains("{operator_pronouns}"));
|
||||
}
|
||||
|
|
@ -250,7 +244,7 @@ shared closer
|
|||
#[test]
|
||||
fn render_no_role_markers_in_output() {
|
||||
// No raw role markers should survive into the rendered prompt.
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, None);
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None);
|
||||
assert!(!rendered.contains("<!-- role:"));
|
||||
assert!(!rendered.contains("<!-- /role:"));
|
||||
// Shared tools appear.
|
||||
|
|
@ -260,13 +254,13 @@ shared closer
|
|||
|
||||
#[test]
|
||||
fn render_uses_agent_opener() {
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, None);
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None);
|
||||
assert!(rendered.starts_with("You are hyperhive agent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_points_agents_at_the_work_dir() {
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, None);
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None);
|
||||
assert!(rendered.contains("`/agents/alice/work/`"), "{rendered}");
|
||||
}
|
||||
|
||||
|
|
@ -280,19 +274,12 @@ shared closer
|
|||
// substitution logic; nothing here depends on the production
|
||||
// template's flavor markers.
|
||||
const IDENTITY_FIXTURE: &str = "\
|
||||
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. Pronouns: **{operator_pronouns}**.
|
||||
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system.
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn render_substitutes_hive_identity_when_set() {
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
"alice",
|
||||
"she/her",
|
||||
Some("pr1ma"),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let rendered = render(IDENTITY_FIXTURE, "alice", Some("pr1ma"), None, None);
|
||||
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
|
||||
// swarm clause stays absent when only hive is set.
|
||||
assert!(!rendered.contains("in swarm"));
|
||||
|
|
@ -303,14 +290,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
|
||||
#[test]
|
||||
fn render_substitutes_swarm_identity_when_set() {
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
"ruth",
|
||||
"she/her",
|
||||
None,
|
||||
Some("constellat1on"),
|
||||
None,
|
||||
);
|
||||
let rendered = render(IDENTITY_FIXTURE, "ruth", None, Some("constellat1on"), None);
|
||||
assert!(rendered.contains("in swarm `constellat1on`"));
|
||||
assert!(!rendered.contains("on hive"));
|
||||
}
|
||||
|
|
@ -320,7 +300,6 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
"iris",
|
||||
"she/her",
|
||||
Some("pr1ma"),
|
||||
Some("constellat1on"),
|
||||
None,
|
||||
|
|
@ -334,7 +313,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
fn render_omits_identity_when_unset() {
|
||||
// None / None must round-trip the non-identity opener verbatim
|
||||
// — single-hive deployments see zero diff.
|
||||
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", None, None, None);
|
||||
let rendered = render(IDENTITY_FIXTURE, "alice", None, None, None);
|
||||
assert!(!rendered.contains("on hive"));
|
||||
assert!(!rendered.contains("in swarm"));
|
||||
assert!(!rendered.contains("{hive_identity}"));
|
||||
|
|
@ -347,14 +326,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
// through `identity::hive_name()` as None (the accessor
|
||||
// filters empty), but `render` should still no-op on a
|
||||
// direct `Some("")` from a test fixture or a future caller.
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
"alice",
|
||||
"she/her",
|
||||
Some(""),
|
||||
Some(""),
|
||||
None,
|
||||
);
|
||||
let rendered = render(IDENTITY_FIXTURE, "alice", Some(""), Some(""), None);
|
||||
assert!(!rendered.contains("on hive"));
|
||||
assert!(!rendered.contains("in swarm"));
|
||||
}
|
||||
|
|
@ -364,7 +336,6 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
let rendered = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
"alice",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
Some("/run/hive-docs"),
|
||||
|
|
@ -380,7 +351,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
#[test]
|
||||
fn render_no_docs_pointer_when_docs_dir_absent_or_empty() {
|
||||
for docs in [None, Some("")] {
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, docs);
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, docs);
|
||||
assert!(
|
||||
!rendered.contains("mounted read-only at"),
|
||||
"docs pointer must not appear for {docs:?}:\n{rendered}"
|
||||
|
|
|
|||
|
|
@ -264,8 +264,8 @@ pub fn stall_sleep_secs() -> u64 {
|
|||
/// Resolve the effective context-window size for watermark calculations.
|
||||
/// Priority order (first wins):
|
||||
/// 1. API-reported window from the last `result` event's `modelUsage.*.contextWindow`.
|
||||
/// 2. `HIVE_CONTEXT_WINDOW_TOKENS_*` env vars (Nix-configured per-model defaults).
|
||||
/// 3. Hard fallback: 200 000.
|
||||
/// 2. `harness_state::context_window_tokens` (per-model defaults, then the
|
||||
/// `HIVE_CONTEXT_WINDOW_TOKENS` override, then 200 000).
|
||||
///
|
||||
/// The API-reported window is the authoritative per-inference active
|
||||
/// context limit. It reflects what the model actually enforces — which
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ pub(super) struct StateSnapshot {
|
|||
/// Effective context-window token budget for the current model.
|
||||
/// Primary source: API-reported `modelUsage.*.contextWindow` from
|
||||
/// the last result event (authoritative per-inference active window).
|
||||
/// Falls back to `HIVE_CONTEXT_WINDOW_TOKENS_*` env vars, then 200 000.
|
||||
/// Falls back to `harness_state::context_window_tokens(model)`.
|
||||
/// Consumers (e.g. dashboard badge) use this to render ctx-usage %.
|
||||
context_window_tokens: u64,
|
||||
/// Last-inference token usage from the most recent completed
|
||||
|
|
|
|||
Loading…
Reference in a new issue