Watch
1
0
Fork
You've already forked souveraine
0
souveraine/src/server/energy.rs

143 lines
5.1 KiB
Rust

use anyhow::Result;
use std::sync::Arc;
use crate::core::nervous::EventBus;
use crate::server::SouveraineServer;
pub(crate) async fn write_energy_balance(server: &Arc<SouveraineServer>, agent_id: &str, event_bus: &EventBus) -> Result<()> {
let memory_root = server.agents.memory_root(agent_id);
let tasks_dir = memory_root.join("tasks");
if !tasks_dir.exists() {
// No tasks directory yet — nothing to count.
return Ok(());
}
let mut generative: usize = 0;
let mut consumptive: usize = 0;
let mut hot: usize = 0;
let mut warm: usize = 0;
let mut cold: usize = 0;
if let Ok(entries) = std::fs::read_dir(&tasks_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map(|e| e == "md").unwrap_or(false) {
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
// Quick frontmatter parse — just the fields we need.
let body = match content.strip_prefix("---\n") {
Some(rest) => match rest.find("\n---\n") {
Some(end) => &rest[..end],
None => continue,
},
None => continue,
};
let mut status: Option<&str> = None;
let mut energy: Option<&str> = None;
let mut momentum: Option<&str> = None;
for line in body.lines() {
if let Some((key, val)) = line.split_once(':') {
let key = key.trim();
let val = val.trim().trim_matches('"');
match key {
"status" => status = Some(val),
"energy" => energy = Some(val),
"momentum" => momentum = Some(val),
_ => {}
}
}
}
// Only live commitments weigh on the energy balance —
// done and cancelled ones have been set down.
if matches!(status, Some("pending") | Some("in_progress")) {
match energy {
Some("generative") => generative += 1,
_ => consumptive += 1,
}
match momentum {
Some("hot") => hot += 1,
Some("warm") => warm += 1,
_ => cold += 1,
}
}
}
}
}
// Determine the top-of-mind description — shifts the tone of the one-liner
// the agent reads in context.
let ratio = if generative + consumptive > 0 {
generative as f32 / (generative + consumptive) as f32
} else {
0.5
};
let description = if generative == 0 && consumptive == 0 {
"no tasks — the space is clean".to_string()
} else if ratio < 0.2 {
"all-consumptive — the engine is running cold".to_string()
} else if ratio < 0.4 {
"mostly obligations — tending the garden".to_string()
} else if ratio > 0.8 {
"all-generative — building new things".to_string()
} else if ratio > 0.6 {
"mostly generative — restless momentum".to_string()
} else {
"balanced — generative and consumptive in rhythm".to_string()
};
let now = chrono::Utc::now();
let frontmatter = format!(
"---\nupdated: {updated}\ngenerative: {gen}\nconsumptive: {con}\nratio: {ratio:.2}\n\
hot: {hot}\nwarm: {warm}\ncold: {cold}\n---\n\n# Energy Balance\n\n\
{gen} generative, {con} consumptive ({hot} hot, {warm} warm, {cold} cold). {desc}\n",
updated = now.to_rfc3339(),
gen = generative,
con = consumptive,
ratio = ratio,
hot = hot,
warm = warm,
cold = cold,
desc = description,
);
let balance_path = memory_root.join("system").join("dynamic").join("energy-balance.md");
if let Some(parent) = balance_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&balance_path, frontmatter)?;
fire_energy_event(event_bus, agent_id, generative, consumptive, ratio);
tracing::debug!(
agent = agent_id,
generative, consumptive,
"energy-balance written"
);
Ok(())
}
/// Fire an energy_balance_updated event so the firehose carries the
/// agent's felt state across machines.
fn fire_energy_event(event_bus: &EventBus, agent_id: &str, generative: usize, consumptive: usize, ratio: f32) {
event_bus.send(crate::core::nervous::SensorEvent {
sensor_name: "energy".into(),
timestamp: chrono::Utc::now(),
event_type: "energy_balance_updated".into(),
target: Some(agent_id.to_string()),
urgency: 0.1,
payload: Some(serde_json::json!({
"generative": generative,
"consumptive": consumptive,
"ratio": ratio,
})),
seed_id: None,
reply_to: None,
});
}