Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9f313e75d | ||
|
|
f8960dd501 | ||
|
|
0a5f35bda3 | ||
|
|
4b5707768c |
29 changed files with 252 additions and 404 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ labels (`hive`, `swarm`, …) ride on the resource via
|
|||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -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_<KEY>` 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 (`<!-- role:agent -->...<!-- /role:agent -->`,
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
if let Some((_, tokens)) = CONTEXT_WINDOW_DEFAULTS
|
||||
.iter()
|
||||
.find(|(family, _)| m.contains(family))
|
||||
{
|
||||
return v;
|
||||
}
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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<Instruments> {
|
|||
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<SdkMeterProvider> {
|
|||
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::<u64>().ok())
|
||||
.filter(|ms| *ms > 0)
|
||||
.map_or(DEFAULT_INTERVAL, Duration::from_millis)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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("<!-- role:"));
|
||||
assert!(!rendered.contains("<!-- /role:"));
|
||||
// Shared tools appear.
|
||||
|
|
@ -260,13 +255,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 +275,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 +291,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 +301,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 +314,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 +327,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 +337,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 +352,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
|
||||
|
|
|
|||
|
|
@ -67,20 +67,6 @@ pub struct Coordinator {
|
|||
/// each per-agent flake so the agent's web UI can build the right
|
||||
/// rebuild-button URL pointing back at the dashboard.
|
||||
pub dashboard_port: u16,
|
||||
/// Operator pronouns (free text) — `she/her` by default, set via
|
||||
/// the NixOS module option `services.hive-c0re.operatorPronouns`.
|
||||
/// Reaches each container as the `HIVE_OPERATOR_PRONOUNS` env var
|
||||
/// (injected into `systemd.services.<harness>.environment` by the
|
||||
/// meta flake); the harness substitutes it into the agent /
|
||||
/// manager system prompt at boot.
|
||||
pub operator_pronouns: String,
|
||||
/// Per-model context-window sizes in tokens. Set via the host-level
|
||||
/// `services.hive-c0re.contextWindowTokens` NixOS option; injected
|
||||
/// into each container as `HIVE_CONTEXT_WINDOW_TOKENS_<KEY_UPPER>`
|
||||
/// by the meta flake renderer. The harness uses these to derive
|
||||
/// compaction / auto-reset watermarks and exposes the active value
|
||||
/// on `/api/state` as `context_window_tokens`.
|
||||
pub context_window_tokens: std::collections::HashMap<String, u64>,
|
||||
/// Per-agent systemd `CPUQuota=` value (e.g. `"200%"`). Written into
|
||||
/// the `container@h-<name>.service.d/` drop-in on every spawn/rebuild.
|
||||
pub agent_cpu_quota: String,
|
||||
|
|
@ -100,9 +86,8 @@ pub struct Coordinator {
|
|||
/// estimate on the ST4TS tab. Set via `services.hyperhive.modelPrices`
|
||||
/// and passed to `hive-c0re serve --model-prices <json>`. Models not
|
||||
/// in the table fall back to the built-in estimate in `hive_stats`.
|
||||
/// hive-c0re-local (read only by the `/api/stats-hive` handler), so —
|
||||
/// unlike `context_window_tokens` — it is *not* part of `HiveEnv` and
|
||||
/// is never injected into containers.
|
||||
/// hive-c0re-local (read only by the `/api/stats-hive` handler), so it
|
||||
/// is *not* part of `HiveEnv` and is never injected into containers.
|
||||
pub model_prices: crate::hive_stats::PriceTable,
|
||||
agents: Mutex<HashMap<String, AgentSocket>>,
|
||||
/// Tombstone for transients that have JUST been cleared. The
|
||||
|
|
@ -224,8 +209,6 @@ pub struct HiveEnv {
|
|||
/// alive host-side by the module that resolved it.
|
||||
pub claude_code_path: Option<String>,
|
||||
pub dashboard_port: u16,
|
||||
pub operator_pronouns: String,
|
||||
pub context_window_tokens: std::collections::HashMap<String, u64>,
|
||||
/// Per-agent systemd `CPUQuota=` value (e.g. `"200%"`).
|
||||
pub agent_cpu_quota: String,
|
||||
/// Per-agent systemd `MemoryMax=` value (e.g. `"4G"`).
|
||||
|
|
@ -247,12 +230,6 @@ impl Default for HiveEnv {
|
|||
nixpkgs_flake: String::new(),
|
||||
claude_code_path: None,
|
||||
dashboard_port: 7000,
|
||||
operator_pronouns: "she/her".to_string(),
|
||||
context_window_tokens: std::collections::HashMap::from([
|
||||
("haiku".to_string(), 200_000),
|
||||
("sonnet".to_string(), 1_000_000),
|
||||
("opus".to_string(), 1_000_000),
|
||||
]),
|
||||
agent_cpu_quota: "200%".to_string(),
|
||||
agent_memory_max: "4G".to_string(),
|
||||
// Slightly below the kernel default of 100, so agent
|
||||
|
|
@ -317,9 +294,7 @@ mod serve_config_tests {
|
|||
assert_eq!(sc.env.hyperhive_flake, "/x");
|
||||
assert_eq!(sc.env.dashboard_port, 1234);
|
||||
// omitted → HiveEnv::default()
|
||||
assert_eq!(sc.env.operator_pronouns, "she/her");
|
||||
assert_eq!(sc.env.agent_cpu_quota, "200%");
|
||||
assert_eq!(sc.env.context_window_tokens.get("sonnet"), Some(&1_000_000));
|
||||
assert!(sc.model_prices.contains_key("opus"));
|
||||
}
|
||||
|
||||
|
|
@ -432,8 +407,6 @@ impl Coordinator {
|
|||
nixpkgs_flake,
|
||||
claude_code_path,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
agent_cpu_quota,
|
||||
agent_memory_max,
|
||||
agent_cpu_weight,
|
||||
|
|
@ -473,8 +446,6 @@ impl Coordinator {
|
|||
nixpkgs_flake,
|
||||
claude_code_path,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
agent_cpu_quota,
|
||||
agent_memory_max,
|
||||
agent_cpu_weight,
|
||||
|
|
@ -506,8 +477,6 @@ impl Coordinator {
|
|||
nixpkgs_flake: self.nixpkgs_flake.clone(),
|
||||
claude_code_path: self.claude_code_path.clone(),
|
||||
dashboard_port: self.dashboard_port,
|
||||
operator_pronouns: self.operator_pronouns.clone(),
|
||||
context_window_tokens: self.context_window_tokens.clone(),
|
||||
agent_cpu_quota: self.agent_cpu_quota.clone(),
|
||||
agent_memory_max: self.agent_memory_max.clone(),
|
||||
agent_cpu_weight: self.agent_cpu_weight,
|
||||
|
|
|
|||
|
|
@ -90,17 +90,6 @@ enum Cmd {
|
|||
/// Override: dashboard HTTP port.
|
||||
#[arg(long)]
|
||||
dashboard_port: Option<u16>,
|
||||
/// Override: operator pronouns (free text). Threaded into each
|
||||
/// container's harness via `HIVE_OPERATOR_PRONOUNS`.
|
||||
#[arg(long)]
|
||||
operator_pronouns: Option<String>,
|
||||
/// Override: per-model context-window sizes, as a JSON object
|
||||
/// mapping model-family short name to token count. Threaded into
|
||||
/// each container as `HIVE_CONTEXT_WINDOW_TOKENS_<KEY_UPPER>` env
|
||||
/// vars. Set via the `services.hive-c0re.contextWindowTokens`
|
||||
/// NixOS option.
|
||||
#[arg(long)]
|
||||
context_window_tokens: Option<String>,
|
||||
/// Override: systemd `CPUQuota=` applied to every agent container
|
||||
/// via a drop-in (e.g. `"200%"` = 2 cores). Set via
|
||||
/// `services.hyperhive.agentCpuQuota`.
|
||||
|
|
@ -150,8 +139,6 @@ async fn main() -> Result<()> {
|
|||
hyperhive_flake,
|
||||
nixpkgs_flake,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
agent_cpu_quota,
|
||||
agent_memory_max,
|
||||
agent_cpu_weight,
|
||||
|
|
@ -180,13 +167,6 @@ async fn main() -> Result<()> {
|
|||
if let Some(v) = dashboard_port {
|
||||
sc.env.dashboard_port = v;
|
||||
}
|
||||
if let Some(v) = operator_pronouns {
|
||||
sc.env.operator_pronouns = v;
|
||||
}
|
||||
if let Some(v) = context_window_tokens {
|
||||
sc.env.context_window_tokens =
|
||||
serde_json::from_str(&v).context("--context-window-tokens: invalid JSON")?;
|
||||
}
|
||||
if let Some(v) = agent_cpu_quota {
|
||||
sc.env.agent_cpu_quota = v;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -334,25 +334,16 @@ fn render_hive_flake(hive: &HiveEnv, agents: &[AgentSpec]) -> String {
|
|||
&hive.nixpkgs_flake,
|
||||
hive.claude_code_path.as_deref(),
|
||||
hive.dashboard_port,
|
||||
&hive.operator_pronouns,
|
||||
&hive.context_window_tokens,
|
||||
agents,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "many genuine flake inputs (source flakes, port, pronouns, tokens, \
|
||||
agents, forge base); a params struct would just move the same fields"
|
||||
)]
|
||||
fn render_flake(
|
||||
hyperhive_flake: &str,
|
||||
docs_flake: &str,
|
||||
nixpkgs_flake: &str,
|
||||
claude_code_path: Option<&str>,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
agents: &[AgentSpec],
|
||||
) -> String {
|
||||
render_flake_with_lookup(
|
||||
|
|
@ -361,8 +352,6 @@ fn render_flake(
|
|||
nixpkgs_flake,
|
||||
claude_code_path,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
agents,
|
||||
agent_canonical_inputs,
|
||||
)
|
||||
|
|
@ -566,10 +555,6 @@ struct OtelConfig {
|
|||
endpoint: String,
|
||||
protocol: String,
|
||||
extra_resource_attributes: Option<String>,
|
||||
metric_interval_ms: Option<u64>,
|
||||
/// `HYPERHIVE_OTEL_DEBUG=1` → `services.hyperhive.agent.otel.debug = true` →
|
||||
/// `CLAUDE_CODE_OTEL_DIAG_STDERR=1` in every agent's env.
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
/// Read the hive-wide OTEL config from env, or `None` when OTEL is off.
|
||||
|
|
@ -589,19 +574,10 @@ fn otel_config() -> Option<OtelConfig> {
|
|||
let extra_resource_attributes = std::env::var("HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES")
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty());
|
||||
let metric_interval_ms = std::env::var("HYPERHIVE_OTEL_METRIC_INTERVAL_MS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.filter(|v| *v > 0);
|
||||
let debug = std::env::var("HYPERHIVE_OTEL_DEBUG")
|
||||
.ok()
|
||||
.is_some_and(|v| v == "1");
|
||||
Some(OtelConfig {
|
||||
endpoint,
|
||||
protocol,
|
||||
extra_resource_attributes,
|
||||
metric_interval_ms,
|
||||
debug,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -742,10 +718,8 @@ fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
|
|||
/// agent flake-lock introspection.
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
clippy::too_many_arguments,
|
||||
reason = "templated string-builder for the meta flake — the length is one \
|
||||
contiguous fmt block, splitting it would just hide the shape; the \
|
||||
args mirror the host-level config the flake is rendered from"
|
||||
contiguous fmt block, splitting it would just hide the shape"
|
||||
)]
|
||||
fn render_flake_with_lookup<F>(
|
||||
hyperhive_flake: &str,
|
||||
|
|
@ -753,8 +727,6 @@ fn render_flake_with_lookup<F>(
|
|||
nixpkgs_flake: &str,
|
||||
claude_code_path: Option<&str>,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
agents: &[AgentSpec],
|
||||
lookup: F,
|
||||
) -> String
|
||||
|
|
@ -830,13 +802,9 @@ where
|
|||
}
|
||||
}
|
||||
out.push_str(" };\n outputs =\n { self, hyperhive, ... }@inputs:\n let\n");
|
||||
// Free-text operator string — escape backslash + double-quote so a
|
||||
// pronouns value like `he/him \ "rare"` round-trips into a valid
|
||||
// nix string literal without breaking the flake.
|
||||
let pronouns_escaped = operator_pronouns.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port }}:"
|
||||
" dashboardPort = {dashboard_port};\n mkAgent = {{ name, isManager, port }}:"
|
||||
);
|
||||
out.push_str(
|
||||
r#" let
|
||||
|
|
@ -921,17 +889,6 @@ where
|
|||
esc(attrs)
|
||||
);
|
||||
}
|
||||
if let Some(ms) = otel.metric_interval_ms {
|
||||
// Int option — emit a bare numeric literal (no quotes). `ms` is a
|
||||
// parsed u64, so it can't inject anything into the rendered nix.
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" services.hyperhive.agent.otel.metricIntervalMs = {ms};"
|
||||
);
|
||||
}
|
||||
if otel.debug {
|
||||
out.push_str(" services.hyperhive.agent.otel.debug = true;\n");
|
||||
}
|
||||
}
|
||||
// Agent-facing service URLs (`services.hyperhive.agent.forge.url`, `services.hyperhive.agent.matrix.url`):
|
||||
// emit the host's real values as build-time agent config, the same
|
||||
|
|
@ -1025,20 +982,8 @@ where
|
|||
HIVE_PORT = toString port;
|
||||
HIVE_LABEL = name;
|
||||
HIVE_DASHBOARD_PORT = toString dashboardPort;
|
||||
HIVE_OPERATOR_PRONOUNS = operatorPronouns;",
|
||||
",
|
||||
);
|
||||
// Per-model context-window env vars declared in the host-level
|
||||
// `services.hive-c0re.contextWindowTokens` option. Use a sorted
|
||||
// iterator for deterministic flake output (no spurious git diffs).
|
||||
let mut sorted_tokens: Vec<(&String, &u64)> = context_window_tokens.iter().collect();
|
||||
sorted_tokens.sort_by_key(|(k, _)| k.as_str());
|
||||
for (key, val) in &sorted_tokens {
|
||||
let upper_key = key.to_ascii_uppercase();
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" HIVE_CONTEXT_WINDOW_TOKENS_{upper_key} = \"{val}\";"
|
||||
);
|
||||
}
|
||||
// Forwarded env vars — picked up from hive-c0re's own systemd unit
|
||||
// env (`services.hyperhive.*` options flow through the hyperhive
|
||||
// NixOS module into the host process). We copy whatever's set into
|
||||
|
|
@ -1314,8 +1259,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
// nixpkgs is a top-level input with an explicit URL; hyperhive
|
||||
|
|
@ -1360,8 +1303,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -1378,8 +1319,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -1418,8 +1357,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
Some("/nix/store/cccc-claude-code-2.1.220"),
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -1446,8 +1383,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -1466,8 +1401,6 @@ mod tests {
|
|||
"",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -1496,8 +1429,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[
|
||||
sample_spec("argus", false, 9001),
|
||||
sample_spec("bitburner", false, 9002),
|
||||
|
|
@ -1529,8 +1460,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
|_| Vec::new(),
|
||||
);
|
||||
|
|
@ -1567,8 +1496,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
unsafe {
|
||||
|
|
@ -1612,8 +1539,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
};
|
||||
|
|
@ -1792,8 +1717,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
unsafe {
|
||||
|
|
@ -1828,8 +1751,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
let want = format!(
|
||||
|
|
@ -1872,8 +1793,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
};
|
||||
|
|
@ -1964,8 +1883,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
};
|
||||
|
|
@ -2038,8 +1955,6 @@ mod tests {
|
|||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
|
|||
use super::container_stats::{self, ContainerResource};
|
||||
use super::warnings;
|
||||
|
||||
/// Default export cadence when `HYPERHIVE_OTEL_METRIC_INTERVAL_MS` is unset.
|
||||
const DEFAULT_INTERVAL: Duration = Duration::from_mins(1);
|
||||
/// Export cadence, and the snapshot refresher's.
|
||||
const EXPORT_INTERVAL: Duration = Duration::from_mins(1);
|
||||
|
||||
/// Latest container-stats snapshot: written by the async refresher, read by
|
||||
/// the sync observable-instrument callbacks.
|
||||
|
|
@ -69,7 +69,7 @@ pub fn spawn_exporter(hyperhive_flake: &str) {
|
|||
let snapshot = SNAPSHOT
|
||||
.get_or_init(|| Arc::new(Mutex::new(Vec::new())))
|
||||
.clone();
|
||||
let interval = interval();
|
||||
let interval = EXPORT_INTERVAL;
|
||||
|
||||
// Async refresher: keep the shared snapshot current for the sync callbacks.
|
||||
let refresh = snapshot.clone();
|
||||
|
|
@ -124,10 +124,9 @@ fn build_provider(
|
|||
.with_protocol(Protocol::HttpJson)
|
||||
.build()
|
||||
.context("build OTLP metric exporter")?;
|
||||
// Drive the reader's export at `interval` so `HYPERHIVE_OTEL_METRIC_INTERVAL_MS`
|
||||
// is the real export cadence (not just the snapshot-refresh cadence). The
|
||||
// refresher runs at the same interval, gather-first, so the snapshot is
|
||||
// populated before the first export.
|
||||
// Drive the reader's export at `interval`, the same cadence as the
|
||||
// refresher. The refresher gathers first, so the snapshot is populated
|
||||
// before the first export.
|
||||
let reader = PeriodicReader::builder(exporter)
|
||||
.with_interval(interval)
|
||||
.build();
|
||||
|
|
@ -400,15 +399,6 @@ fn endpoint() -> Option<String> {
|
|||
.filter(|s| !s.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::<u64>().ok())
|
||||
.filter(|ms| *ms > 0)
|
||||
.map_or(DEFAULT_INTERVAL, Duration::from_millis)
|
||||
}
|
||||
|
||||
/// Extra resource attributes from `HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES`
|
||||
/// (`key=value,key=value`), same env the agent-side export reads.
|
||||
fn resource_attributes() -> Vec<(String, String)> {
|
||||
|
|
|
|||
|
|
@ -55,11 +55,10 @@ impl HttpClient for BoxedHttpClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// Default export cadence when `HYPERHIVE_OTEL_METRIC_INTERVAL_MS` is unset.
|
||||
/// Matches `hive-c0re::stats::otel_metrics`'s default — no reason for the
|
||||
/// exporters across this tree to disagree on how fresh "current" means by
|
||||
/// default.
|
||||
const DEFAULT_INTERVAL: Duration = Duration::from_mins(1);
|
||||
/// Export cadence, and the snapshot refresher's. Matches
|
||||
/// `hive-c0re::stats::otel_metrics` — no reason for the exporters across
|
||||
/// this tree to disagree on how fresh "current" means.
|
||||
const EXPORT_INTERVAL: Duration = Duration::from_mins(1);
|
||||
|
||||
/// Spawn a jobq-rollup OTEL exporter for `jobq` if OTEL is configured
|
||||
/// (`OTEL_EXPORTER_OTLP_ENDPOINT` non-empty) — `None` otherwise, same
|
||||
|
|
@ -97,7 +96,7 @@ where
|
|||
{
|
||||
let endpoint = endpoint()?;
|
||||
let snapshot: Arc<Mutex<Vec<StateCount>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let interval = interval();
|
||||
let interval = EXPORT_INTERVAL;
|
||||
|
||||
let refresh = snapshot.clone();
|
||||
tokio::spawn(async move {
|
||||
|
|
@ -225,18 +224,6 @@ fn endpoint() -> Option<String> {
|
|||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// Export cadence from `HYPERHIVE_OTEL_METRIC_INTERVAL_MS`, else the
|
||||
/// default. Same variable `hive-c0re::stats::otel_metrics` reads — one
|
||||
/// cadence knob for every OTEL exporter in the tree, not a new one per
|
||||
/// module.
|
||||
fn interval() -> Duration {
|
||||
std::env::var("HYPERHIVE_OTEL_METRIC_INTERVAL_MS")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u64>().ok())
|
||||
.filter(|ms| *ms > 0)
|
||||
.map_or(DEFAULT_INTERVAL, Duration::from_millis)
|
||||
}
|
||||
|
||||
/// Extra resource attributes from `HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES`
|
||||
/// (`key=value,key=value`), same env `hive-c0re::stats::otel_metrics` reads.
|
||||
fn resource_attributes() -> Vec<(String, String)> {
|
||||
|
|
|
|||
|
|
@ -70,11 +70,9 @@ in
|
|||
`swarmctl agent inference set`) wins over this option.
|
||||
|
||||
Valid values are the short model names that `claude --model` accepts:
|
||||
`"haiku"`, `"sonnet"`, `"opus"` (or any future identifier). Context
|
||||
window sizes are looked up at runtime from the
|
||||
`HIVE_CONTEXT_WINDOW_TOKENS_<KEY_UPPER>` env vars injected by the
|
||||
meta flake; override sizes via `services.hyperhive.c0re.contextWindowTokens`
|
||||
on the host.
|
||||
`"haiku"`, `"sonnet"`, `"opus"` (or any future identifier). The
|
||||
harness takes the context window from the model's own report after
|
||||
each turn, and from a per-family default before the first.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ in
|
|||
baseClaudeEnv
|
||||
// lib.optionalAttrs otelCfg.enable otelSettingsEnv
|
||||
// lib.optionalAttrs (otelCfg.enable && otelCfg.debug) {
|
||||
# SDK diagnostics — noisy; only on when services.hyperhive.otel.debug = true.
|
||||
# SDK diagnostics — noisy; only on when services.hyperhive.agent.otel.debug = true.
|
||||
CLAUDE_CODE_OTEL_DIAG_STDERR = "1";
|
||||
};
|
||||
in
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ in
|
|||
./memory-cap.nix
|
||||
./network.nix
|
||||
./otel.nix
|
||||
./otel-removed.nix
|
||||
./packages.nix
|
||||
./queue.nix
|
||||
./queue-identity.nix
|
||||
|
|
@ -280,9 +281,6 @@ in
|
|||
# here so both the harness binary and any user-shell `cargo run`
|
||||
# inside the container resolve them from the same path.
|
||||
# SHELL must be set so claude's Bash tool finds a POSIX shell.
|
||||
# HIVE_CONTEXT_WINDOW_TOKENS_* are injected by the meta flake from the
|
||||
# host-level `services.hyperhive.c0re.contextWindowTokens` option — not
|
||||
# set here.
|
||||
environment.variables = {
|
||||
HIVE_ASSETS_DIR = "${config.services.hyperhive.agent.packages.assets}/share/hyperhive";
|
||||
SHELL = "${pkgs.bashInteractive}/bin/bash";
|
||||
|
|
|
|||
27
nix/agent-modules/otel-removed.nix
Normal file
27
nix/agent-modules/otel-removed.nix
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Removal shim for `services.hyperhive.agent.otel.metricIntervalMs`.
|
||||
#
|
||||
# A definition is ignored and produces a warning, so an agent config, or a
|
||||
# meta flake rendered before the option went, keeps evaluating. claude runs
|
||||
# at the OTEL SDK's default export cadence.
|
||||
#
|
||||
# Self-contained and deletable: one file to remove when the deprecation
|
||||
# window closes.
|
||||
{ lib, config, ... }:
|
||||
let
|
||||
value = config.services.hyperhive.agent.otel.metricIntervalMs;
|
||||
in
|
||||
{
|
||||
options.services.hyperhive.agent.otel.metricIntervalMs = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
visible = false;
|
||||
internal = true;
|
||||
description = "Removed and ignored. `null` is \"not set\".";
|
||||
};
|
||||
|
||||
config.warnings = lib.optional (value != null) ''
|
||||
services.hyperhive.agent.otel.metricIntervalMs is removed and ignored:
|
||||
claude exports metrics at the SDK default of 60 seconds. Remove this
|
||||
setting.
|
||||
'';
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
# OpenTelemetry wiring for an agent container: the per-agent options the
|
||||
# meta flake injects (host-driven from `services.hyperhive.otel.*`), the
|
||||
# meta flake injects (host-driven from `services.hyperhive.otel.*`) plus
|
||||
# `debug`, which the agent's own config sets, the
|
||||
# environment every OTLP producer inside the container reads, and the
|
||||
# collector that forwards this container's journal to the hive.
|
||||
#
|
||||
|
|
@ -62,9 +63,6 @@ let
|
|||
# silently drop without a deltatocumulative processor.
|
||||
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = "cumulative";
|
||||
OTEL_RESOURCE_ATTRIBUTES = resourceAttributes;
|
||||
}
|
||||
// lib.optionalAttrs (cfg.metricIntervalMs != null) {
|
||||
OTEL_METRIC_EXPORT_INTERVAL = toString cfg.metricIntervalMs;
|
||||
};
|
||||
|
||||
# The exporter's NAME picks the wire protocol, so `protocol` has to be
|
||||
|
|
@ -131,25 +129,14 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
metricIntervalMs = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
internal = true;
|
||||
description = ''
|
||||
Metric export interval in milliseconds, set as
|
||||
`OTEL_METRIC_EXPORT_INTERVAL`. Null leaves the SDK default (60s
|
||||
for Claude Code). Host-driven via
|
||||
`services.hyperhive.otel.metricIntervalMs`.
|
||||
'';
|
||||
};
|
||||
|
||||
debug = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
internal = true;
|
||||
description = ''
|
||||
Emit OTEL SDK diagnostics to stderr (`CLAUDE_CODE_OTEL_DIAG_STDERR=1`).
|
||||
Host-driven via `services.hyperhive.otel.debug`.
|
||||
Emit claude's OTEL SDK diagnostics to stderr
|
||||
(`CLAUDE_CODE_OTEL_DIAG_STDERR=1`). Set it in this agent's own
|
||||
config while troubleshooting its collector connection; it is noisy,
|
||||
so leave it off otherwise. Only takes effect when `enable` is true.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue