hive-agent: export a per-file line count for subagent role files
A role file is read into every subagent dispatch that names it, and nothing measured its size. A new watch on the same 15-minute tick as claude_md_watch records `hyperhive.agent.subagent_role.lines`, one series per `<role>.md` under the state dir's `subagent_roles/`, labelled `role` with the file stem. Metric only: no todo, no threshold. Fixes #4558
This commit is contained in:
parent
74b63dd031
commit
f19a9f546e
4 changed files with 123 additions and 6 deletions
93
hive-agent/src/subagent_role_watch.rs
Normal file
93
hive-agent/src/subagent_role_watch.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
//! Subagent role-file size watch: feeds each role file's line count into
|
||||
//! an OTEL gauge, one series per role, on the same ~15-minute tick as
|
||||
//! [`crate::claude_md_watch`]. Metric only — no todo, no threshold.
|
||||
//!
|
||||
//! A role is read into every subagent dispatch that names it, so one
|
||||
//! role's growth is the signal; a per-file series shows which role grew,
|
||||
//! where a total would not.
|
||||
//!
|
||||
//! Role files are `<role>.md` directly under `subagent_roles/` in the
|
||||
//! agent's state dir — the directory `hive-subagent-mcp`'s
|
||||
//! `paths::subagent_roles_dir` loads them from. An absent directory is
|
||||
//! the ordinary state for an agent with no roles and exports nothing.
|
||||
//!
|
||||
//! A role file deleted while the harness runs keeps its last value until
|
||||
//! the harness restarts: a synchronous gauge has no way to retract a
|
||||
//! series.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Same cadence as `claude_md_watch`.
|
||||
const CHECK_INTERVAL: Duration = Duration::from_mins(15);
|
||||
/// Extension every role file carries; the role name is the file stem.
|
||||
const ROLE_EXT: &str = "md";
|
||||
|
||||
/// Background loop: periodically record every role file's line count.
|
||||
/// Detached task — runs for the harness's lifetime; errors are logged,
|
||||
/// never fatal.
|
||||
pub async fn run() {
|
||||
loop {
|
||||
tokio::time::sleep(CHECK_INTERVAL).await;
|
||||
match tokio::task::spawn_blocking(|| role_line_counts(&roles_dir())).await {
|
||||
Ok(counts) => {
|
||||
for (role, lines) in counts {
|
||||
crate::otel_turn_metrics::record_subagent_role_lines(role, lines);
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "subagent role watch probe panicked"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The agent's role directory, resolved the same way as
|
||||
/// `hive-subagent-mcp`'s `paths::subagent_roles_dir`.
|
||||
fn roles_dir() -> PathBuf {
|
||||
crate::paths::state_dir().join("subagent_roles")
|
||||
}
|
||||
|
||||
/// `(role, line count)` for every readable `<role>.md` in `dir`. Empty when
|
||||
/// `dir` is absent or unreadable — no roles yet is not a warning.
|
||||
fn role_line_counts(dir: &Path) -> Vec<(String, u64)> {
|
||||
std::fs::read_dir(dir)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
if path.extension()?.to_str()? != ROLE_EXT {
|
||||
return None;
|
||||
}
|
||||
let role = path.file_stem()?.to_str()?.to_owned();
|
||||
let contents = std::fs::read_to_string(&path).ok()?;
|
||||
Some((role, contents.lines().count() as u64))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn one_series_per_role_file_labelled_by_stem() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
std::fs::write(dir.path().join("reviewer.md"), "one\ntwo\nthree\n").expect("write");
|
||||
std::fs::write(dir.path().join("scout.md"), "a\nb\nc\nd\ne").expect("write");
|
||||
// Not a role file: wrong extension.
|
||||
std::fs::write(dir.path().join("notes.txt"), "x\n").expect("write");
|
||||
|
||||
let mut counts = role_line_counts(dir.path());
|
||||
counts.sort();
|
||||
assert_eq!(
|
||||
counts,
|
||||
vec![("reviewer".to_owned(), 3), ("scout".to_owned(), 5)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_dir_gives_no_series() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
assert!(role_line_counts(&dir.path().join("subagent_roles")).is_empty());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue