diff --git a/README.md b/README.md index d8e5f3ce..3e420c60 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,6 @@ covers spreading a swarm across hosts. swarm.domain = "hive.example"; # the swarm's DNS domain hiveName = "pr1ma"; # this hive's label in it swarm.hives.pr1ma = { }; # the directory: just us - - # c0re.operatorPronouns = "they/them"; # default "she/her" }; # ... the rest of your host config diff --git a/docs/scheduler/observability.md b/docs/scheduler/observability.md index 299a16db..91788e96 100644 --- a/docs/scheduler/observability.md +++ b/docs/scheduler/observability.md @@ -325,7 +325,7 @@ labels (`hive`, `swarm`, …) ride on the resource via -Cadence follows `metricIntervalMs` (default 60s). Transport is OTLP/HTTP +The export cadence is a fixed 60s. Transport is OTLP/HTTP (JSON) to the hive collector's bridge address, with no auth header — that first hop is unauthenticated for every producer on this host, and the upstream credential stays on the swarm tier. @@ -409,8 +409,8 @@ window used by agent** panel (health tab) divides one by the other. Resource attributes (`service.name`, `agent`, `swarm`) come from the same container-wide `OTEL_RESOURCE_ATTRIBUTES` as everything else in this section, and the swarm's collector adds `hive` — nothing extra to configure. -Cadence follows `HYPERHIVE_OTEL_METRIC_INTERVAL_MS` (default 60s, same variable + default as -`hive-c0re`'s container-resource export above) — that only controls how often +The export cadence is a fixed 60s, the same +as `hive-c0re`'s container-resource export above. That only controls how often the harness flushes the batched points to the collector, not how often it records them (every turn, always). @@ -486,8 +486,8 @@ store either a commit or a push anywhere as a row to count. A push with zero commits (a branch delete, or a force-push that doesn't add new commits) still increments `push.count`; `commit.count` only advances -when the delivery actually carries commits. Same enable signal (`OTEL_EXPORTER_OTLP_ENDPOINT`), cadence variable -(`HYPERHIVE_OTEL_METRIC_INTERVAL_MS`) and `HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES` +when the delivery actually carries commits. Same enable signal (`OTEL_EXPORTER_OTLP_ENDPOINT`), fixed 60s cadence +and `HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES` resource-attribute channel as `swarm-controller`'s other OTEL exporter (its `hive-jobq-metrics`-backed job-graph rollup, undocumented here — see that crate's own doc comment) — `swarm-controller` sets `service.name = diff --git a/docs/turn-loop/claude-invocation.md b/docs/turn-loop/claude-invocation.md index bdbc0742..992b6314 100644 --- a/docs/turn-loop/claude-invocation.md +++ b/docs/turn-loop/claude-invocation.md @@ -61,16 +61,15 @@ clearing it goes back to the local value → context-window size per-model. Resolution order (first match wins): -1. `HIVE_CONTEXT_WINDOW_TOKENS_` env var, where `KEY` - (lowercased) is a substring of the active model name. Injected - by the meta flake from `services.hyperhive.c0re.contextWindowTokens` - (host-level NixOS option, defaults: haiku=200k, sonnet=1M, - opus=1M). Override these for all agents at once without a - per-agent config change. +1. The harness's per-family defaults, matched when the family name + is a case-insensitive substring of the active model name: + haiku=200k, sonnet=1M, opus=1M. 2. `HIVE_CONTEXT_WINDOW_TOKENS` — single global override for any - model (useful in dev / test). -3. Hard fallback: `200_000` (conservative; only reached outside - NixOS where the env vars aren't set). + other model (useful in dev / test). +3. Hard fallback: `200_000`. + +Once a turn completes, the window the model reports +(`modelUsage.*.contextWindow`) replaces this value. The effective window drives watermarks; the harness exposes it at runtime via `/api/state.context_window_tokens` so the UI can show a @@ -256,14 +255,13 @@ needs to `chown` a bind mount), once at startup: `hive-agent/prompts/system.md` by `hive-agent`'s `prompt::render`: HTML-comment markers (`...`, same for `role:manager`) gate the role-specific blocks; both flavors - share everything else. Five placeholders are then + share everything else. Four placeholders are then substituted: `{label}` (short agent name), `{qualified_label}` - (hive-qualified `name@domain` form), `{operator_pronouns}`, + (hive-qualified `name@domain` form), `{hive_identity}` (for example `` on hive `pr1ma` ``; empty when `HYPERHIVE_HIVE_NAME` is unset), and `{swarm_identity}` (same - shape for the swarm). Pronouns come from `HIVE_OPERATOR_PRONOUNS` - env (set by the meta flake from - `services.hyperhive.c0re.operatorPronouns`, default `she/her`). + shape for the swarm). The template names no pronouns for the + operator, and tells agents to call them the operator. When you enable `services.hyperhive.agent.docs.enable`, `HIVE_DOCS_DIR` is present in the environment and `render()` appends a one-sentence pointer telling the agent where the docs live. diff --git a/hive-agent/prompts/system.md b/hive-agent/prompts/system.md index 71042c8d..1fce9027 100644 --- a/hive-agent/prompts/system.md +++ b/hive-agent/prompts/system.md @@ -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 is the human at the dashboard; say "the operator" when you mention the operator 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: diff --git a/hive-agent/src/events.rs b/hive-agent/src/events.rs index 0722a535..05da0568 100644 --- a/hive-agent/src/events.rs +++ b/hive-agent/src/events.rs @@ -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>>, /// Resolved model id from the most recent turn's `assistant` events /// (the API-echoed `message.model`, e.g. `claude-opus-4-8`), as diff --git a/hive-agent/src/harness_state.rs b/hive-agent/src/harness_state.rs index 23e447e0..d6dedf7e 100644 --- a/hive-agent/src/harness_state.rs +++ b/hive-agent/src/harness_state.rs @@ -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_` 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 (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::() - && 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() { diff --git a/hive-agent/src/otel_turn_metrics.rs b/hive-agent/src/otel_turn_metrics.rs index 6cf96ff6..18ef9246 100644 --- a/hive-agent/src/otel_turn_metrics.rs +++ b/hive-agent/src/otel_turn_metrics.rs @@ -39,10 +39,9 @@ use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider}; use crate::turn_stats::TurnStatRow; -/// Default export cadence when `HYPERHIVE_OTEL_METRIC_INTERVAL_MS` is unset. -/// Matches `hive-c0re`'s container-metrics default so the two exporters -/// batch on the same rhythm by default. -const DEFAULT_INTERVAL: Duration = Duration::from_mins(1); +/// Export cadence. Matches `hive-c0re`'s container-metrics exporter so the +/// two batch on the same rhythm. +const EXPORT_INTERVAL: Duration = Duration::from_mins(1); struct Instruments { /// Kept alive for the process lifetime — the `PeriodicReader` only @@ -177,7 +176,7 @@ fn build() -> Option { tracing::debug!("otel turn-metrics: no endpoint configured, exporter disabled"); return None; } - let interval = interval(); + let interval = EXPORT_INTERVAL; match build_provider(interval) { Ok(provider) => { tracing::info!(?interval, "otel turn-metrics: exporter enabled"); @@ -254,12 +253,3 @@ fn build_provider(interval: Duration) -> anyhow::Result { pub fn enabled() -> bool { std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_ok_and(|s| !s.trim().is_empty()) } - -/// Export cadence from `HYPERHIVE_OTEL_METRIC_INTERVAL_MS`, else the default. -fn interval() -> Duration { - std::env::var("HYPERHIVE_OTEL_METRIC_INTERVAL_MS") - .ok() - .and_then(|s| s.trim().parse::().ok()) - .filter(|ms| *ms > 0) - .map_or(DEFAULT_INTERVAL, Duration::from_millis) -} diff --git a/hive-agent/src/prompt.rs b/hive-agent/src/prompt.rs index 0d7efb35..7ca58e82 100644 --- a/hive-agent/src/prompt.rs +++ b/hive-agent/src/prompt.rs @@ -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 { 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 let body = render( &template, label, - &pronouns, hive_name.as_deref(), swarm_name.as_deref(), docs_dir.as_deref(), @@ -236,13 +232,12 @@ 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_names_no_operator_pronouns() { + // 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("say \"the operator\"")); + assert!(!rendered.contains("pronoun")); assert!(!rendered.contains("{label}")); assert!(!rendered.contains("{operator_pronouns}")); } @@ -250,7 +245,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("