Compare commits
29 changed files with 404 additions and 252 deletions
|
|
@ -136,6 +136,8 @@ covers spreading a swarm across hosts.
|
||||||
swarm.domain = "hive.example"; # the swarm's DNS domain
|
swarm.domain = "hive.example"; # the swarm's DNS domain
|
||||||
hiveName = "pr1ma"; # this hive's label in it
|
hiveName = "pr1ma"; # this hive's label in it
|
||||||
swarm.hives.pr1ma = { }; # the directory: just us
|
swarm.hives.pr1ma = { }; # the directory: just us
|
||||||
|
|
||||||
|
# c0re.operatorPronouns = "they/them"; # default "she/her"
|
||||||
};
|
};
|
||||||
|
|
||||||
# ... the rest of your host config
|
# ... the rest of your host config
|
||||||
|
|
|
||||||
|
|
@ -325,7 +325,7 @@ labels (`hive`, `swarm`, …) ride on the resource via
|
||||||
|
|
||||||
<!-- vale write-good.Passive = NO -->
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
The export cadence is a fixed 60s. Transport is OTLP/HTTP
|
Cadence follows `metricIntervalMs` (default 60s). Transport is OTLP/HTTP
|
||||||
(JSON) to the hive collector's bridge address, with no auth header — that
|
(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
|
first hop is unauthenticated for every producer on this host, and the upstream
|
||||||
credential stays on the swarm tier.
|
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
|
Resource attributes (`service.name`, `agent`, `swarm`) come from the
|
||||||
same container-wide `OTEL_RESOURCE_ATTRIBUTES` as everything else in this
|
same container-wide `OTEL_RESOURCE_ATTRIBUTES` as everything else in this
|
||||||
section, and the swarm's collector adds `hive` — nothing extra to configure.
|
section, and the swarm's collector adds `hive` — nothing extra to configure.
|
||||||
The export cadence is a fixed 60s, the same
|
Cadence follows `HYPERHIVE_OTEL_METRIC_INTERVAL_MS` (default 60s, same variable + default as
|
||||||
as `hive-c0re`'s container-resource export above. That only controls how often
|
`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
|
the harness flushes the batched points to the collector, not how often it
|
||||||
records them (every turn, always).
|
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
|
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
|
new commits) still increments `push.count`; `commit.count` only advances
|
||||||
when the delivery actually carries commits. Same enable signal (`OTEL_EXPORTER_OTLP_ENDPOINT`), fixed 60s cadence
|
when the delivery actually carries commits. Same enable signal (`OTEL_EXPORTER_OTLP_ENDPOINT`), cadence variable
|
||||||
and `HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES`
|
(`HYPERHIVE_OTEL_METRIC_INTERVAL_MS`) and `HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES`
|
||||||
resource-attribute channel as `swarm-controller`'s other OTEL exporter (its
|
resource-attribute channel as `swarm-controller`'s other OTEL exporter (its
|
||||||
`hive-jobq-metrics`-backed job-graph rollup, undocumented here — see that
|
`hive-jobq-metrics`-backed job-graph rollup, undocumented here — see that
|
||||||
crate's own doc comment) — `swarm-controller` sets `service.name =
|
crate's own doc comment) — `swarm-controller` sets `service.name =
|
||||||
|
|
|
||||||
|
|
@ -61,15 +61,16 @@ clearing it goes back to the local value →
|
||||||
context-window size per-model. Resolution order (first
|
context-window size per-model. Resolution order (first
|
||||||
match wins):
|
match wins):
|
||||||
|
|
||||||
1. The harness's per-family defaults, matched when the family name
|
1. `HIVE_CONTEXT_WINDOW_TOKENS_<KEY>` env var, where `KEY`
|
||||||
is a case-insensitive substring of the active model name:
|
(lowercased) is a substring of the active model name. Injected
|
||||||
haiku=200k, sonnet=1M, opus=1M.
|
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.
|
||||||
2. `HIVE_CONTEXT_WINDOW_TOKENS` — single global override for any
|
2. `HIVE_CONTEXT_WINDOW_TOKENS` — single global override for any
|
||||||
other model (useful in dev / test).
|
model (useful in dev / test).
|
||||||
3. Hard fallback: `200_000`.
|
3. Hard fallback: `200_000` (conservative; only reached outside
|
||||||
|
NixOS where the env vars aren't set).
|
||||||
Once a turn completes, the window the model reports
|
|
||||||
(`modelUsage.*.contextWindow`) replaces this value.
|
|
||||||
|
|
||||||
The effective window drives watermarks; the harness exposes it at
|
The effective window drives watermarks; the harness exposes it at
|
||||||
runtime via `/api/state.context_window_tokens` so the UI can show a
|
runtime via `/api/state.context_window_tokens` so the UI can show a
|
||||||
|
|
@ -255,13 +256,14 @@ needs to `chown` a bind mount), once at startup:
|
||||||
`hive-agent/prompts/system.md` by `hive-agent`'s `prompt::render`:
|
`hive-agent/prompts/system.md` by `hive-agent`'s `prompt::render`:
|
||||||
HTML-comment markers (`<!-- role:agent -->...<!-- /role:agent -->`,
|
HTML-comment markers (`<!-- role:agent -->...<!-- /role:agent -->`,
|
||||||
same for `role:manager`) gate the role-specific blocks; both flavors
|
same for `role:manager`) gate the role-specific blocks; both flavors
|
||||||
share everything else. Four placeholders are then
|
share everything else. Five placeholders are then
|
||||||
substituted: `{label}` (short agent name), `{qualified_label}`
|
substituted: `{label}` (short agent name), `{qualified_label}`
|
||||||
(hive-qualified `name@domain` form),
|
(hive-qualified `name@domain` form), `{operator_pronouns}`,
|
||||||
`{hive_identity}` (for example `` on hive `pr1ma` ``; empty when
|
`{hive_identity}` (for example `` on hive `pr1ma` ``; empty when
|
||||||
`HYPERHIVE_HIVE_NAME` is unset), and `{swarm_identity}` (same
|
`HYPERHIVE_HIVE_NAME` is unset), and `{swarm_identity}` (same
|
||||||
shape for the swarm). The template names no pronouns for the
|
shape for the swarm). Pronouns come from `HIVE_OPERATOR_PRONOUNS`
|
||||||
operator, and tells agents to call them the operator.
|
env (set by the meta flake from
|
||||||
|
`services.hyperhive.c0re.operatorPronouns`, default `she/her`).
|
||||||
When you enable `services.hyperhive.agent.docs.enable`, `HIVE_DOCS_DIR` is present
|
When you enable `services.hyperhive.agent.docs.enable`, `HIVE_DOCS_DIR` is present
|
||||||
in the environment and `render()` appends a one-sentence pointer
|
in the environment and `render()` appends a one-sentence pointer
|
||||||
telling the agent where the docs live.
|
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 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.
|
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.
|
||||||
|
|
||||||
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:
|
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
|
/// Per-inference context-window size as reported by the Anthropic API
|
||||||
/// in the stream-json `result` event (`modelUsage.*.contextWindow`).
|
/// in the stream-json `result` event (`modelUsage.*.contextWindow`).
|
||||||
/// Set by the stdout pump on every completed turn. Takes precedence
|
/// Set by the stdout pump on every completed turn. Takes precedence
|
||||||
/// over the per-model defaults in `harness_state::context_window_tokens`
|
/// over the Nix-configured `HIVE_CONTEXT_WINDOW_TOKENS_*` env vars
|
||||||
/// for compaction watermark calculations — it reflects the actual
|
/// for compaction watermark calculations — it reflects the actual
|
||||||
/// limit the model enforces, which may differ from the default
|
/// limit the model enforces, which may differ from what the operator
|
||||||
/// (e.g. 200 k active window on a 1 M cache-enabled model).
|
/// configured (e.g. 200 k active window on a 1 M cache-enabled model).
|
||||||
api_context_window: Arc<Mutex<Option<u64>>>,
|
api_context_window: Arc<Mutex<Option<u64>>>,
|
||||||
/// Resolved model id from the most recent turn's `assistant` events
|
/// Resolved model id from the most recent turn's `assistant` events
|
||||||
/// (the API-echoed `message.model`, e.g. `claude-opus-4-8`), as
|
/// (the API-echoed `message.model`, e.g. `claude-opus-4-8`), as
|
||||||
|
|
|
||||||
|
|
@ -237,31 +237,33 @@ pub fn configured_effort() -> Option<&'static str> {
|
||||||
.map(|s| &*Box::leak(s.into_boxed_str()))
|
.map(|s| &*Box::leak(s.into_boxed_str()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Context-window size in tokens per model family, keyed by a substring
|
/// Context-window size in tokens for a given model name.
|
||||||
/// of the model name (`"sonnet"` matches `"claude-sonnet-4-5"`).
|
///
|
||||||
const CONTEXT_WINDOW_DEFAULTS: [(&str, u64); 3] = [
|
/// Canonical per-model sizes are declared in the harness nix modules as
|
||||||
("haiku", 200_000),
|
/// `hyperhive.contextWindowTokens` and injected as
|
||||||
("sonnet", 1_000_000),
|
/// `HIVE_CONTEXT_WINDOW_TOKENS_<KEY_UPPER>` env vars — so this function
|
||||||
("opus", 1_000_000),
|
/// normally just reads them. The Rust code carries no model knowledge;
|
||||||
];
|
/// updating model families only requires a Nix change.
|
||||||
|
|
||||||
/// 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):
|
/// Resolution order (first match wins):
|
||||||
/// 1. [`CONTEXT_WINDOW_DEFAULTS`] — the family key, case-insensitively,
|
/// 1. `HIVE_CONTEXT_WINDOW_TOKENS_<KEY>` — key (lowercased) is a
|
||||||
/// is a substring of the model name.
|
/// substring of the active model name. Populated by the Nix default
|
||||||
|
/// map for all known families; add/override in `agent.nix`.
|
||||||
/// 2. `HIVE_CONTEXT_WINDOW_TOKENS` — single global override (any model).
|
/// 2. `HIVE_CONTEXT_WINDOW_TOKENS` — single global override (any model).
|
||||||
/// 3. Hard fallback: `200_000`.
|
/// 3. Hard fallback: `200_000` (conservative; only hit outside NixOS).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn context_window_tokens(model: &str) -> u64 {
|
pub fn context_window_tokens(model: &str) -> u64 {
|
||||||
let m = model.to_ascii_lowercase();
|
let m = model.to_ascii_lowercase();
|
||||||
if let Some((_, tokens)) = CONTEXT_WINDOW_DEFAULTS
|
// Per-model env vars set by `hyperhive.contextWindowTokens` in Nix.
|
||||||
.iter()
|
for (key, val) in std::env::vars() {
|
||||||
.find(|(family, _)| m.contains(family))
|
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 *tokens;
|
return v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Global override (single value, any model).
|
// Global override (single value, any model).
|
||||||
if let Ok(s) = std::env::var("HIVE_CONTEXT_WINDOW_TOKENS")
|
if let Ok(s) = std::env::var("HIVE_CONTEXT_WINDOW_TOKENS")
|
||||||
|
|
@ -270,25 +272,13 @@ pub fn context_window_tokens(model: &str) -> u64 {
|
||||||
{
|
{
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
// Hard fallback for dev/test outside NixOS where env vars aren't set.
|
||||||
200_000
|
200_000
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{DEFAULT_EFFORT, DEFAULT_MODEL, Source, effort_from, model_from};
|
||||||
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]
|
#[test]
|
||||||
fn compiled_default_effort_matches_claude_settings() {
|
fn compiled_default_effort_matches_claude_settings() {
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,10 @@ use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
|
||||||
|
|
||||||
use crate::turn_stats::TurnStatRow;
|
use crate::turn_stats::TurnStatRow;
|
||||||
|
|
||||||
/// Export cadence. Matches `hive-c0re`'s container-metrics exporter so the
|
/// Default export cadence when `HYPERHIVE_OTEL_METRIC_INTERVAL_MS` is unset.
|
||||||
/// two batch on the same rhythm.
|
/// Matches `hive-c0re`'s container-metrics default so the two exporters
|
||||||
const EXPORT_INTERVAL: Duration = Duration::from_mins(1);
|
/// batch on the same rhythm by default.
|
||||||
|
const DEFAULT_INTERVAL: Duration = Duration::from_mins(1);
|
||||||
|
|
||||||
struct Instruments {
|
struct Instruments {
|
||||||
/// Kept alive for the process lifetime — the `PeriodicReader` only
|
/// Kept alive for the process lifetime — the `PeriodicReader` only
|
||||||
|
|
@ -176,7 +177,7 @@ fn build() -> Option<Instruments> {
|
||||||
tracing::debug!("otel turn-metrics: no endpoint configured, exporter disabled");
|
tracing::debug!("otel turn-metrics: no endpoint configured, exporter disabled");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let interval = EXPORT_INTERVAL;
|
let interval = interval();
|
||||||
match build_provider(interval) {
|
match build_provider(interval) {
|
||||||
Ok(provider) => {
|
Ok(provider) => {
|
||||||
tracing::info!(?interval, "otel turn-metrics: exporter enabled");
|
tracing::info!(?interval, "otel turn-metrics: exporter enabled");
|
||||||
|
|
@ -253,3 +254,12 @@ fn build_provider(interval: Duration) -> anyhow::Result<SdkMeterProvider> {
|
||||||
pub fn enabled() -> bool {
|
pub fn enabled() -> bool {
|
||||||
std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_ok_and(|s| !s.trim().is_empty())
|
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};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
/// Assemble the system prompt for a given label + optional hive /
|
/// Assemble the system prompt for a given label + pronouns + optional hive /
|
||||||
/// swarm display names. Pure function — no I/O. Splits out from
|
/// swarm display names. Pure function — no I/O. Splits out from
|
||||||
/// [`write_system_prompt`] so the marker logic + substitution is unit-testable
|
/// [`write_system_prompt`] so the marker logic + substitution is unit-testable
|
||||||
/// in isolation. The caller supplies the template body so tests can pass an
|
/// in isolation. The caller supplies the template body so tests can pass an
|
||||||
|
|
@ -21,6 +21,7 @@ use anyhow::{Context, Result};
|
||||||
pub fn render(
|
pub fn render(
|
||||||
template: &str,
|
template: &str,
|
||||||
label: &str,
|
label: &str,
|
||||||
|
operator_pronouns: &str,
|
||||||
hive_name: Option<&str>,
|
hive_name: Option<&str>,
|
||||||
swarm_name: Option<&str>,
|
swarm_name: Option<&str>,
|
||||||
docs_dir: Option<&str>,
|
docs_dir: Option<&str>,
|
||||||
|
|
@ -36,6 +37,7 @@ pub fn render(
|
||||||
let rendered = body
|
let rendered = body
|
||||||
.replace("{label}", label)
|
.replace("{label}", label)
|
||||||
.replace("{qualified_label}", &qualified)
|
.replace("{qualified_label}", &qualified)
|
||||||
|
.replace("{operator_pronouns}", operator_pronouns)
|
||||||
.replace("{hive_identity}", &hive_identity)
|
.replace("{hive_identity}", &hive_identity)
|
||||||
.replace("{swarm_identity}", &swarm_identity);
|
.replace("{swarm_identity}", &swarm_identity);
|
||||||
// When the reference docs are mounted in-container (`services.hyperhive.agent.docs.enable`
|
// When the reference docs are mounted in-container (`services.hyperhive.agent.docs.enable`
|
||||||
|
|
@ -112,6 +114,7 @@ fn parse_close_marker(line: &str) -> Option<&str> {
|
||||||
pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf> {
|
pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf> {
|
||||||
let parent = crate::paths::config_dir();
|
let parent = crate::paths::config_dir();
|
||||||
tokio::fs::create_dir_all(&parent).await.ok();
|
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_path = hive_sh4re::assets::prompt_template();
|
||||||
let template = tokio::fs::read_to_string(&template_path)
|
let template = tokio::fs::read_to_string(&template_path)
|
||||||
.await
|
.await
|
||||||
|
|
@ -134,6 +137,7 @@ pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf>
|
||||||
let body = render(
|
let body = render(
|
||||||
&template,
|
&template,
|
||||||
label,
|
label,
|
||||||
|
&pronouns,
|
||||||
hive_name.as_deref(),
|
hive_name.as_deref(),
|
||||||
swarm_name.as_deref(),
|
swarm_name.as_deref(),
|
||||||
docs_dir.as_deref(),
|
docs_dir.as_deref(),
|
||||||
|
|
@ -232,12 +236,13 @@ shared closer
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_substitutes_label_and_names_no_operator_pronouns() {
|
fn render_substitutes_label_and_pronouns() {
|
||||||
// Real template's first agent line.
|
// Real template's first agent line — keeps the renderer
|
||||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None);
|
// honest about the {label} / {operator_pronouns} pair the
|
||||||
|
// harness already relied on.
|
||||||
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "they/them", None, None, None);
|
||||||
assert!(rendered.contains("hyperhive agent `alice`"));
|
assert!(rendered.contains("hyperhive agent `alice`"));
|
||||||
assert!(rendered.contains("say \"the operator\""));
|
assert!(rendered.contains("**they/them** pronouns"));
|
||||||
assert!(!rendered.contains("pronoun"));
|
|
||||||
assert!(!rendered.contains("{label}"));
|
assert!(!rendered.contains("{label}"));
|
||||||
assert!(!rendered.contains("{operator_pronouns}"));
|
assert!(!rendered.contains("{operator_pronouns}"));
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +250,7 @@ shared closer
|
||||||
#[test]
|
#[test]
|
||||||
fn render_no_role_markers_in_output() {
|
fn render_no_role_markers_in_output() {
|
||||||
// No raw role markers should survive into the rendered prompt.
|
// No raw role markers should survive into the rendered prompt.
|
||||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None);
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, None);
|
||||||
assert!(!rendered.contains("<!-- role:"));
|
assert!(!rendered.contains("<!-- role:"));
|
||||||
assert!(!rendered.contains("<!-- /role:"));
|
assert!(!rendered.contains("<!-- /role:"));
|
||||||
// Shared tools appear.
|
// Shared tools appear.
|
||||||
|
|
@ -255,13 +260,13 @@ shared closer
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_uses_agent_opener() {
|
fn render_uses_agent_opener() {
|
||||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None);
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, None);
|
||||||
assert!(rendered.starts_with("You are hyperhive agent"));
|
assert!(rendered.starts_with("You are hyperhive agent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_points_agents_at_the_work_dir() {
|
fn render_points_agents_at_the_work_dir() {
|
||||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None);
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, None);
|
||||||
assert!(rendered.contains("`/agents/alice/work/`"), "{rendered}");
|
assert!(rendered.contains("`/agents/alice/work/`"), "{rendered}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,12 +280,19 @@ shared closer
|
||||||
// substitution logic; nothing here depends on the production
|
// substitution logic; nothing here depends on the production
|
||||||
// template's flavor markers.
|
// template's flavor markers.
|
||||||
const IDENTITY_FIXTURE: &str = "\
|
const IDENTITY_FIXTURE: &str = "\
|
||||||
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system.
|
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. Pronouns: **{operator_pronouns}**.
|
||||||
";
|
";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_substitutes_hive_identity_when_set() {
|
fn render_substitutes_hive_identity_when_set() {
|
||||||
let rendered = render(IDENTITY_FIXTURE, "alice", Some("pr1ma"), None, None);
|
let rendered = render(
|
||||||
|
IDENTITY_FIXTURE,
|
||||||
|
"alice",
|
||||||
|
"she/her",
|
||||||
|
Some("pr1ma"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
|
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
|
||||||
// swarm clause stays absent when only hive is set.
|
// swarm clause stays absent when only hive is set.
|
||||||
assert!(!rendered.contains("in swarm"));
|
assert!(!rendered.contains("in swarm"));
|
||||||
|
|
@ -291,7 +303,14 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_substitutes_swarm_identity_when_set() {
|
fn render_substitutes_swarm_identity_when_set() {
|
||||||
let rendered = render(IDENTITY_FIXTURE, "ruth", None, Some("constellat1on"), None);
|
let rendered = render(
|
||||||
|
IDENTITY_FIXTURE,
|
||||||
|
"ruth",
|
||||||
|
"she/her",
|
||||||
|
None,
|
||||||
|
Some("constellat1on"),
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(rendered.contains("in swarm `constellat1on`"));
|
assert!(rendered.contains("in swarm `constellat1on`"));
|
||||||
assert!(!rendered.contains("on hive"));
|
assert!(!rendered.contains("on hive"));
|
||||||
}
|
}
|
||||||
|
|
@ -301,6 +320,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
let rendered = render(
|
let rendered = render(
|
||||||
IDENTITY_FIXTURE,
|
IDENTITY_FIXTURE,
|
||||||
"iris",
|
"iris",
|
||||||
|
"she/her",
|
||||||
Some("pr1ma"),
|
Some("pr1ma"),
|
||||||
Some("constellat1on"),
|
Some("constellat1on"),
|
||||||
None,
|
None,
|
||||||
|
|
@ -314,7 +334,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
fn render_omits_identity_when_unset() {
|
fn render_omits_identity_when_unset() {
|
||||||
// None / None must round-trip the non-identity opener verbatim
|
// None / None must round-trip the non-identity opener verbatim
|
||||||
// — single-hive deployments see zero diff.
|
// — single-hive deployments see zero diff.
|
||||||
let rendered = render(IDENTITY_FIXTURE, "alice", None, None, None);
|
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", None, None, None);
|
||||||
assert!(!rendered.contains("on hive"));
|
assert!(!rendered.contains("on hive"));
|
||||||
assert!(!rendered.contains("in swarm"));
|
assert!(!rendered.contains("in swarm"));
|
||||||
assert!(!rendered.contains("{hive_identity}"));
|
assert!(!rendered.contains("{hive_identity}"));
|
||||||
|
|
@ -327,7 +347,14 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
// through `identity::hive_name()` as None (the accessor
|
// through `identity::hive_name()` as None (the accessor
|
||||||
// filters empty), but `render` should still no-op on a
|
// filters empty), but `render` should still no-op on a
|
||||||
// direct `Some("")` from a test fixture or a future caller.
|
// direct `Some("")` from a test fixture or a future caller.
|
||||||
let rendered = render(IDENTITY_FIXTURE, "alice", Some(""), Some(""), None);
|
let rendered = render(
|
||||||
|
IDENTITY_FIXTURE,
|
||||||
|
"alice",
|
||||||
|
"she/her",
|
||||||
|
Some(""),
|
||||||
|
Some(""),
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(!rendered.contains("on hive"));
|
assert!(!rendered.contains("on hive"));
|
||||||
assert!(!rendered.contains("in swarm"));
|
assert!(!rendered.contains("in swarm"));
|
||||||
}
|
}
|
||||||
|
|
@ -337,6 +364,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
let rendered = render(
|
let rendered = render(
|
||||||
&PRODUCTION_TEMPLATE,
|
&PRODUCTION_TEMPLATE,
|
||||||
"alice",
|
"alice",
|
||||||
|
"she/her",
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some("/run/hive-docs"),
|
Some("/run/hive-docs"),
|
||||||
|
|
@ -352,7 +380,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
#[test]
|
#[test]
|
||||||
fn render_no_docs_pointer_when_docs_dir_absent_or_empty() {
|
fn render_no_docs_pointer_when_docs_dir_absent_or_empty() {
|
||||||
for docs in [None, Some("")] {
|
for docs in [None, Some("")] {
|
||||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, docs);
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, docs);
|
||||||
assert!(
|
assert!(
|
||||||
!rendered.contains("mounted read-only at"),
|
!rendered.contains("mounted read-only at"),
|
||||||
"docs pointer must not appear for {docs:?}:\n{rendered}"
|
"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.
|
/// Resolve the effective context-window size for watermark calculations.
|
||||||
/// Priority order (first wins):
|
/// Priority order (first wins):
|
||||||
/// 1. API-reported window from the last `result` event's `modelUsage.*.contextWindow`.
|
/// 1. API-reported window from the last `result` event's `modelUsage.*.contextWindow`.
|
||||||
/// 2. `harness_state::context_window_tokens` (per-model defaults, then the
|
/// 2. `HIVE_CONTEXT_WINDOW_TOKENS_*` env vars (Nix-configured per-model defaults).
|
||||||
/// `HIVE_CONTEXT_WINDOW_TOKENS` override, then 200 000).
|
/// 3. Hard fallback: 200 000.
|
||||||
///
|
///
|
||||||
/// The API-reported window is the authoritative per-inference active
|
/// The API-reported window is the authoritative per-inference active
|
||||||
/// context limit. It reflects what the model actually enforces — which
|
/// 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.
|
/// Effective context-window token budget for the current model.
|
||||||
/// Primary source: API-reported `modelUsage.*.contextWindow` from
|
/// Primary source: API-reported `modelUsage.*.contextWindow` from
|
||||||
/// the last result event (authoritative per-inference active window).
|
/// the last result event (authoritative per-inference active window).
|
||||||
/// Falls back to `harness_state::context_window_tokens(model)`.
|
/// Falls back to `HIVE_CONTEXT_WINDOW_TOKENS_*` env vars, then 200 000.
|
||||||
/// Consumers (e.g. dashboard badge) use this to render ctx-usage %.
|
/// Consumers (e.g. dashboard badge) use this to render ctx-usage %.
|
||||||
context_window_tokens: u64,
|
context_window_tokens: u64,
|
||||||
/// Last-inference token usage from the most recent completed
|
/// Last-inference token usage from the most recent completed
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,20 @@ pub struct Coordinator {
|
||||||
/// each per-agent flake so the agent's web UI can build the right
|
/// each per-agent flake so the agent's web UI can build the right
|
||||||
/// rebuild-button URL pointing back at the dashboard.
|
/// rebuild-button URL pointing back at the dashboard.
|
||||||
pub dashboard_port: u16,
|
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
|
/// Per-agent systemd `CPUQuota=` value (e.g. `"200%"`). Written into
|
||||||
/// the `container@h-<name>.service.d/` drop-in on every spawn/rebuild.
|
/// the `container@h-<name>.service.d/` drop-in on every spawn/rebuild.
|
||||||
pub agent_cpu_quota: String,
|
pub agent_cpu_quota: String,
|
||||||
|
|
@ -86,8 +100,9 @@ pub struct Coordinator {
|
||||||
/// estimate on the ST4TS tab. Set via `services.hyperhive.modelPrices`
|
/// estimate on the ST4TS tab. Set via `services.hyperhive.modelPrices`
|
||||||
/// and passed to `hive-c0re serve --model-prices <json>`. Models not
|
/// and passed to `hive-c0re serve --model-prices <json>`. Models not
|
||||||
/// in the table fall back to the built-in estimate in `hive_stats`.
|
/// 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 it
|
/// hive-c0re-local (read only by the `/api/stats-hive` handler), so —
|
||||||
/// is *not* part of `HiveEnv` and is never injected into containers.
|
/// unlike `context_window_tokens` — it is *not* part of `HiveEnv` and
|
||||||
|
/// is never injected into containers.
|
||||||
pub model_prices: crate::hive_stats::PriceTable,
|
pub model_prices: crate::hive_stats::PriceTable,
|
||||||
agents: Mutex<HashMap<String, AgentSocket>>,
|
agents: Mutex<HashMap<String, AgentSocket>>,
|
||||||
/// Tombstone for transients that have JUST been cleared. The
|
/// Tombstone for transients that have JUST been cleared. The
|
||||||
|
|
@ -209,6 +224,8 @@ pub struct HiveEnv {
|
||||||
/// alive host-side by the module that resolved it.
|
/// alive host-side by the module that resolved it.
|
||||||
pub claude_code_path: Option<String>,
|
pub claude_code_path: Option<String>,
|
||||||
pub dashboard_port: u16,
|
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%"`).
|
/// Per-agent systemd `CPUQuota=` value (e.g. `"200%"`).
|
||||||
pub agent_cpu_quota: String,
|
pub agent_cpu_quota: String,
|
||||||
/// Per-agent systemd `MemoryMax=` value (e.g. `"4G"`).
|
/// Per-agent systemd `MemoryMax=` value (e.g. `"4G"`).
|
||||||
|
|
@ -230,6 +247,12 @@ impl Default for HiveEnv {
|
||||||
nixpkgs_flake: String::new(),
|
nixpkgs_flake: String::new(),
|
||||||
claude_code_path: None,
|
claude_code_path: None,
|
||||||
dashboard_port: 7000,
|
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_cpu_quota: "200%".to_string(),
|
||||||
agent_memory_max: "4G".to_string(),
|
agent_memory_max: "4G".to_string(),
|
||||||
// Slightly below the kernel default of 100, so agent
|
// Slightly below the kernel default of 100, so agent
|
||||||
|
|
@ -294,7 +317,9 @@ mod serve_config_tests {
|
||||||
assert_eq!(sc.env.hyperhive_flake, "/x");
|
assert_eq!(sc.env.hyperhive_flake, "/x");
|
||||||
assert_eq!(sc.env.dashboard_port, 1234);
|
assert_eq!(sc.env.dashboard_port, 1234);
|
||||||
// omitted → HiveEnv::default()
|
// omitted → HiveEnv::default()
|
||||||
|
assert_eq!(sc.env.operator_pronouns, "she/her");
|
||||||
assert_eq!(sc.env.agent_cpu_quota, "200%");
|
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"));
|
assert!(sc.model_prices.contains_key("opus"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,6 +432,8 @@ impl Coordinator {
|
||||||
nixpkgs_flake,
|
nixpkgs_flake,
|
||||||
claude_code_path,
|
claude_code_path,
|
||||||
dashboard_port,
|
dashboard_port,
|
||||||
|
operator_pronouns,
|
||||||
|
context_window_tokens,
|
||||||
agent_cpu_quota,
|
agent_cpu_quota,
|
||||||
agent_memory_max,
|
agent_memory_max,
|
||||||
agent_cpu_weight,
|
agent_cpu_weight,
|
||||||
|
|
@ -446,6 +473,8 @@ impl Coordinator {
|
||||||
nixpkgs_flake,
|
nixpkgs_flake,
|
||||||
claude_code_path,
|
claude_code_path,
|
||||||
dashboard_port,
|
dashboard_port,
|
||||||
|
operator_pronouns,
|
||||||
|
context_window_tokens,
|
||||||
agent_cpu_quota,
|
agent_cpu_quota,
|
||||||
agent_memory_max,
|
agent_memory_max,
|
||||||
agent_cpu_weight,
|
agent_cpu_weight,
|
||||||
|
|
@ -477,6 +506,8 @@ impl Coordinator {
|
||||||
nixpkgs_flake: self.nixpkgs_flake.clone(),
|
nixpkgs_flake: self.nixpkgs_flake.clone(),
|
||||||
claude_code_path: self.claude_code_path.clone(),
|
claude_code_path: self.claude_code_path.clone(),
|
||||||
dashboard_port: self.dashboard_port,
|
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_cpu_quota: self.agent_cpu_quota.clone(),
|
||||||
agent_memory_max: self.agent_memory_max.clone(),
|
agent_memory_max: self.agent_memory_max.clone(),
|
||||||
agent_cpu_weight: self.agent_cpu_weight,
|
agent_cpu_weight: self.agent_cpu_weight,
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,17 @@ enum Cmd {
|
||||||
/// Override: dashboard HTTP port.
|
/// Override: dashboard HTTP port.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
dashboard_port: Option<u16>,
|
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
|
/// Override: systemd `CPUQuota=` applied to every agent container
|
||||||
/// via a drop-in (e.g. `"200%"` = 2 cores). Set via
|
/// via a drop-in (e.g. `"200%"` = 2 cores). Set via
|
||||||
/// `services.hyperhive.agentCpuQuota`.
|
/// `services.hyperhive.agentCpuQuota`.
|
||||||
|
|
@ -139,6 +150,8 @@ async fn main() -> Result<()> {
|
||||||
hyperhive_flake,
|
hyperhive_flake,
|
||||||
nixpkgs_flake,
|
nixpkgs_flake,
|
||||||
dashboard_port,
|
dashboard_port,
|
||||||
|
operator_pronouns,
|
||||||
|
context_window_tokens,
|
||||||
agent_cpu_quota,
|
agent_cpu_quota,
|
||||||
agent_memory_max,
|
agent_memory_max,
|
||||||
agent_cpu_weight,
|
agent_cpu_weight,
|
||||||
|
|
@ -167,6 +180,13 @@ async fn main() -> Result<()> {
|
||||||
if let Some(v) = dashboard_port {
|
if let Some(v) = dashboard_port {
|
||||||
sc.env.dashboard_port = v;
|
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 {
|
if let Some(v) = agent_cpu_quota {
|
||||||
sc.env.agent_cpu_quota = v;
|
sc.env.agent_cpu_quota = v;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -334,16 +334,25 @@ fn render_hive_flake(hive: &HiveEnv, agents: &[AgentSpec]) -> String {
|
||||||
&hive.nixpkgs_flake,
|
&hive.nixpkgs_flake,
|
||||||
hive.claude_code_path.as_deref(),
|
hive.claude_code_path.as_deref(),
|
||||||
hive.dashboard_port,
|
hive.dashboard_port,
|
||||||
|
&hive.operator_pronouns,
|
||||||
|
&hive.context_window_tokens,
|
||||||
agents,
|
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(
|
fn render_flake(
|
||||||
hyperhive_flake: &str,
|
hyperhive_flake: &str,
|
||||||
docs_flake: &str,
|
docs_flake: &str,
|
||||||
nixpkgs_flake: &str,
|
nixpkgs_flake: &str,
|
||||||
claude_code_path: Option<&str>,
|
claude_code_path: Option<&str>,
|
||||||
dashboard_port: u16,
|
dashboard_port: u16,
|
||||||
|
operator_pronouns: &str,
|
||||||
|
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||||
agents: &[AgentSpec],
|
agents: &[AgentSpec],
|
||||||
) -> String {
|
) -> String {
|
||||||
render_flake_with_lookup(
|
render_flake_with_lookup(
|
||||||
|
|
@ -352,6 +361,8 @@ fn render_flake(
|
||||||
nixpkgs_flake,
|
nixpkgs_flake,
|
||||||
claude_code_path,
|
claude_code_path,
|
||||||
dashboard_port,
|
dashboard_port,
|
||||||
|
operator_pronouns,
|
||||||
|
context_window_tokens,
|
||||||
agents,
|
agents,
|
||||||
agent_canonical_inputs,
|
agent_canonical_inputs,
|
||||||
)
|
)
|
||||||
|
|
@ -555,6 +566,10 @@ struct OtelConfig {
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
protocol: String,
|
protocol: String,
|
||||||
extra_resource_attributes: Option<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.
|
/// Read the hive-wide OTEL config from env, or `None` when OTEL is off.
|
||||||
|
|
@ -574,10 +589,19 @@ fn otel_config() -> Option<OtelConfig> {
|
||||||
let extra_resource_attributes = std::env::var("HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES")
|
let extra_resource_attributes = std::env::var("HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|v| !v.is_empty());
|
.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 {
|
Some(OtelConfig {
|
||||||
endpoint,
|
endpoint,
|
||||||
protocol,
|
protocol,
|
||||||
extra_resource_attributes,
|
extra_resource_attributes,
|
||||||
|
metric_interval_ms,
|
||||||
|
debug,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -718,8 +742,10 @@ fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
|
||||||
/// agent flake-lock introspection.
|
/// agent flake-lock introspection.
|
||||||
#[allow(
|
#[allow(
|
||||||
clippy::too_many_lines,
|
clippy::too_many_lines,
|
||||||
|
clippy::too_many_arguments,
|
||||||
reason = "templated string-builder for the meta flake — the length is one \
|
reason = "templated string-builder for the meta flake — the length is one \
|
||||||
contiguous fmt block, splitting it would just hide the shape"
|
contiguous fmt block, splitting it would just hide the shape; the \
|
||||||
|
args mirror the host-level config the flake is rendered from"
|
||||||
)]
|
)]
|
||||||
fn render_flake_with_lookup<F>(
|
fn render_flake_with_lookup<F>(
|
||||||
hyperhive_flake: &str,
|
hyperhive_flake: &str,
|
||||||
|
|
@ -727,6 +753,8 @@ fn render_flake_with_lookup<F>(
|
||||||
nixpkgs_flake: &str,
|
nixpkgs_flake: &str,
|
||||||
claude_code_path: Option<&str>,
|
claude_code_path: Option<&str>,
|
||||||
dashboard_port: u16,
|
dashboard_port: u16,
|
||||||
|
operator_pronouns: &str,
|
||||||
|
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||||
agents: &[AgentSpec],
|
agents: &[AgentSpec],
|
||||||
lookup: F,
|
lookup: F,
|
||||||
) -> String
|
) -> String
|
||||||
|
|
@ -802,9 +830,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.push_str(" };\n outputs =\n { self, hyperhive, ... }@inputs:\n let\n");
|
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!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
" dashboardPort = {dashboard_port};\n mkAgent = {{ name, isManager, port }}:"
|
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port }}:"
|
||||||
);
|
);
|
||||||
out.push_str(
|
out.push_str(
|
||||||
r#" let
|
r#" let
|
||||||
|
|
@ -889,6 +921,17 @@ where
|
||||||
esc(attrs)
|
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`):
|
// 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
|
// emit the host's real values as build-time agent config, the same
|
||||||
|
|
@ -982,8 +1025,20 @@ where
|
||||||
HIVE_PORT = toString port;
|
HIVE_PORT = toString port;
|
||||||
HIVE_LABEL = name;
|
HIVE_LABEL = name;
|
||||||
HIVE_DASHBOARD_PORT = toString dashboardPort;
|
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
|
// Forwarded env vars — picked up from hive-c0re's own systemd unit
|
||||||
// env (`services.hyperhive.*` options flow through the hyperhive
|
// env (`services.hyperhive.*` options flow through the hyperhive
|
||||||
// NixOS module into the host process). We copy whatever's set into
|
// NixOS module into the host process). We copy whatever's set into
|
||||||
|
|
@ -1259,6 +1314,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
);
|
);
|
||||||
// nixpkgs is a top-level input with an explicit URL; hyperhive
|
// nixpkgs is a top-level input with an explicit URL; hyperhive
|
||||||
|
|
@ -1303,6 +1360,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -1319,6 +1378,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -1357,6 +1418,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
Some("/nix/store/cccc-claude-code-2.1.220"),
|
Some("/nix/store/cccc-claude-code-2.1.220"),
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -1383,6 +1446,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -1401,6 +1466,8 @@ mod tests {
|
||||||
"",
|
"",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -1429,6 +1496,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[
|
&[
|
||||||
sample_spec("argus", false, 9001),
|
sample_spec("argus", false, 9001),
|
||||||
sample_spec("bitburner", false, 9002),
|
sample_spec("bitburner", false, 9002),
|
||||||
|
|
@ -1460,6 +1529,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
|_| Vec::new(),
|
|_| Vec::new(),
|
||||||
);
|
);
|
||||||
|
|
@ -1496,6 +1567,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
);
|
);
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|
@ -1539,6 +1612,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -1717,6 +1792,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
);
|
);
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|
@ -1751,6 +1828,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
);
|
);
|
||||||
let want = format!(
|
let want = format!(
|
||||||
|
|
@ -1793,6 +1872,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -1883,6 +1964,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -1955,6 +2038,8 @@ mod tests {
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
"path:/nix/store/aaaa-nixpkgs-source",
|
||||||
None,
|
None,
|
||||||
8000,
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
&[sample_spec("alice", false, 9001)],
|
&[sample_spec("alice", false, 9001)],
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
|
||||||
use super::container_stats::{self, ContainerResource};
|
use super::container_stats::{self, ContainerResource};
|
||||||
use super::warnings;
|
use super::warnings;
|
||||||
|
|
||||||
/// Export cadence, and the snapshot refresher's.
|
/// Default export cadence when `HYPERHIVE_OTEL_METRIC_INTERVAL_MS` is unset.
|
||||||
const EXPORT_INTERVAL: Duration = Duration::from_mins(1);
|
const DEFAULT_INTERVAL: Duration = Duration::from_mins(1);
|
||||||
|
|
||||||
/// Latest container-stats snapshot: written by the async refresher, read by
|
/// Latest container-stats snapshot: written by the async refresher, read by
|
||||||
/// the sync observable-instrument callbacks.
|
/// the sync observable-instrument callbacks.
|
||||||
|
|
@ -69,7 +69,7 @@ pub fn spawn_exporter(hyperhive_flake: &str) {
|
||||||
let snapshot = SNAPSHOT
|
let snapshot = SNAPSHOT
|
||||||
.get_or_init(|| Arc::new(Mutex::new(Vec::new())))
|
.get_or_init(|| Arc::new(Mutex::new(Vec::new())))
|
||||||
.clone();
|
.clone();
|
||||||
let interval = EXPORT_INTERVAL;
|
let interval = interval();
|
||||||
|
|
||||||
// Async refresher: keep the shared snapshot current for the sync callbacks.
|
// Async refresher: keep the shared snapshot current for the sync callbacks.
|
||||||
let refresh = snapshot.clone();
|
let refresh = snapshot.clone();
|
||||||
|
|
@ -124,9 +124,10 @@ fn build_provider(
|
||||||
.with_protocol(Protocol::HttpJson)
|
.with_protocol(Protocol::HttpJson)
|
||||||
.build()
|
.build()
|
||||||
.context("build OTLP metric exporter")?;
|
.context("build OTLP metric exporter")?;
|
||||||
// Drive the reader's export at `interval`, the same cadence as the
|
// Drive the reader's export at `interval` so `HYPERHIVE_OTEL_METRIC_INTERVAL_MS`
|
||||||
// refresher. The refresher gathers first, so the snapshot is populated
|
// is the real export cadence (not just the snapshot-refresh cadence). The
|
||||||
// before the first export.
|
// refresher runs at the same interval, gather-first, so the snapshot is
|
||||||
|
// populated before the first export.
|
||||||
let reader = PeriodicReader::builder(exporter)
|
let reader = PeriodicReader::builder(exporter)
|
||||||
.with_interval(interval)
|
.with_interval(interval)
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -399,6 +400,15 @@ fn endpoint() -> Option<String> {
|
||||||
.filter(|s| !s.is_empty())
|
.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`
|
/// Extra resource attributes from `HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES`
|
||||||
/// (`key=value,key=value`), same env the agent-side export reads.
|
/// (`key=value,key=value`), same env the agent-side export reads.
|
||||||
fn resource_attributes() -> Vec<(String, String)> {
|
fn resource_attributes() -> Vec<(String, String)> {
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,11 @@ impl HttpClient for BoxedHttpClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Export cadence, and the snapshot refresher's. Matches
|
/// Default export cadence when `HYPERHIVE_OTEL_METRIC_INTERVAL_MS` is unset.
|
||||||
/// `hive-c0re::stats::otel_metrics` — no reason for the exporters across
|
/// Matches `hive-c0re::stats::otel_metrics`'s default — no reason for the
|
||||||
/// this tree to disagree on how fresh "current" means.
|
/// exporters across this tree to disagree on how fresh "current" means by
|
||||||
const EXPORT_INTERVAL: Duration = Duration::from_mins(1);
|
/// default.
|
||||||
|
const DEFAULT_INTERVAL: Duration = Duration::from_mins(1);
|
||||||
|
|
||||||
/// Spawn a jobq-rollup OTEL exporter for `jobq` if OTEL is configured
|
/// Spawn a jobq-rollup OTEL exporter for `jobq` if OTEL is configured
|
||||||
/// (`OTEL_EXPORTER_OTLP_ENDPOINT` non-empty) — `None` otherwise, same
|
/// (`OTEL_EXPORTER_OTLP_ENDPOINT` non-empty) — `None` otherwise, same
|
||||||
|
|
@ -96,7 +97,7 @@ where
|
||||||
{
|
{
|
||||||
let endpoint = endpoint()?;
|
let endpoint = endpoint()?;
|
||||||
let snapshot: Arc<Mutex<Vec<StateCount>>> = Arc::new(Mutex::new(Vec::new()));
|
let snapshot: Arc<Mutex<Vec<StateCount>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
let interval = EXPORT_INTERVAL;
|
let interval = interval();
|
||||||
|
|
||||||
let refresh = snapshot.clone();
|
let refresh = snapshot.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|
@ -224,6 +225,18 @@ fn endpoint() -> Option<String> {
|
||||||
.filter(|s| !s.is_empty())
|
.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`
|
/// Extra resource attributes from `HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES`
|
||||||
/// (`key=value,key=value`), same env `hive-c0re::stats::otel_metrics` reads.
|
/// (`key=value,key=value`), same env `hive-c0re::stats::otel_metrics` reads.
|
||||||
fn resource_attributes() -> Vec<(String, String)> {
|
fn resource_attributes() -> Vec<(String, String)> {
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,11 @@ in
|
||||||
`swarmctl agent inference set`) wins over this option.
|
`swarmctl agent inference set`) wins over this option.
|
||||||
|
|
||||||
Valid values are the short model names that `claude --model` accepts:
|
Valid values are the short model names that `claude --model` accepts:
|
||||||
`"haiku"`, `"sonnet"`, `"opus"` (or any future identifier). The
|
`"haiku"`, `"sonnet"`, `"opus"` (or any future identifier). Context
|
||||||
harness takes the context window from the model's own report after
|
window sizes are looked up at runtime from the
|
||||||
each turn, and from a per-family default before the first.
|
`HIVE_CONTEXT_WINDOW_TOKENS_<KEY_UPPER>` env vars injected by the
|
||||||
|
meta flake; override sizes via `services.hyperhive.c0re.contextWindowTokens`
|
||||||
|
on the host.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ in
|
||||||
baseClaudeEnv
|
baseClaudeEnv
|
||||||
// lib.optionalAttrs otelCfg.enable otelSettingsEnv
|
// lib.optionalAttrs otelCfg.enable otelSettingsEnv
|
||||||
// lib.optionalAttrs (otelCfg.enable && otelCfg.debug) {
|
// lib.optionalAttrs (otelCfg.enable && otelCfg.debug) {
|
||||||
# SDK diagnostics — noisy; only on when services.hyperhive.agent.otel.debug = true.
|
# SDK diagnostics — noisy; only on when services.hyperhive.otel.debug = true.
|
||||||
CLAUDE_CODE_OTEL_DIAG_STDERR = "1";
|
CLAUDE_CODE_OTEL_DIAG_STDERR = "1";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ in
|
||||||
./memory-cap.nix
|
./memory-cap.nix
|
||||||
./network.nix
|
./network.nix
|
||||||
./otel.nix
|
./otel.nix
|
||||||
./otel-removed.nix
|
|
||||||
./packages.nix
|
./packages.nix
|
||||||
./queue.nix
|
./queue.nix
|
||||||
./queue-identity.nix
|
./queue-identity.nix
|
||||||
|
|
@ -281,6 +280,9 @@ in
|
||||||
# here so both the harness binary and any user-shell `cargo run`
|
# here so both the harness binary and any user-shell `cargo run`
|
||||||
# inside the container resolve them from the same path.
|
# inside the container resolve them from the same path.
|
||||||
# SHELL must be set so claude's Bash tool finds a POSIX shell.
|
# 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 = {
|
environment.variables = {
|
||||||
HIVE_ASSETS_DIR = "${config.services.hyperhive.agent.packages.assets}/share/hyperhive";
|
HIVE_ASSETS_DIR = "${config.services.hyperhive.agent.packages.assets}/share/hyperhive";
|
||||||
SHELL = "${pkgs.bashInteractive}/bin/bash";
|
SHELL = "${pkgs.bashInteractive}/bin/bash";
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# 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,6 +1,5 @@
|
||||||
# OpenTelemetry wiring for an agent container: the per-agent options the
|
# OpenTelemetry wiring for an agent container: the per-agent options the
|
||||||
# meta flake injects (host-driven from `services.hyperhive.otel.*`) plus
|
# meta flake injects (host-driven from `services.hyperhive.otel.*`), the
|
||||||
# `debug`, which the agent's own config sets, the
|
|
||||||
# environment every OTLP producer inside the container reads, and the
|
# environment every OTLP producer inside the container reads, and the
|
||||||
# collector that forwards this container's journal to the hive.
|
# collector that forwards this container's journal to the hive.
|
||||||
#
|
#
|
||||||
|
|
@ -63,6 +62,9 @@ let
|
||||||
# silently drop without a deltatocumulative processor.
|
# silently drop without a deltatocumulative processor.
|
||||||
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = "cumulative";
|
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = "cumulative";
|
||||||
OTEL_RESOURCE_ATTRIBUTES = resourceAttributes;
|
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
|
# The exporter's NAME picks the wire protocol, so `protocol` has to be
|
||||||
|
|
@ -129,14 +131,25 @@ 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 {
|
debug = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
|
internal = true;
|
||||||
description = ''
|
description = ''
|
||||||
Emit claude's OTEL SDK diagnostics to stderr
|
Emit OTEL SDK diagnostics to stderr (`CLAUDE_CODE_OTEL_DIAG_STDERR=1`).
|
||||||
(`CLAUDE_CODE_OTEL_DIAG_STDERR=1`). Set it in this agent's own
|
Host-driven via `services.hyperhive.otel.debug`.
|
||||||
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