use std::path::Path;
use crate::app::Snapshot;
use crate::budgets::{BudgetSpend, Scope, total, wall};
use crate::transcript::{Block, Entry, EntryKind, Transcript};
const GOAL_FILE: &str = "goal.md";
const AGENTS_DIR: &str = "agents";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct Observed {
pub(super) goal: Option<String>,
pub(super) governing: Option<String>,
pub(super) usage: BudgetSpend,
pub(super) wall_secs: u64,
pub(super) steps: usize,
pub(super) response: Option<String>,
pub(super) verdicts: Vec<super::Verdict>,
pub(super) compacted: usize,
}
pub(super) fn observed(snap: &Snapshot, workspace: &Path, agent: &str) -> Observed {
let bills: Vec<_> = snap
.bills
.get(workspace)
.map(|all| {
let scope = Scope::Tree(agent.to_owned());
all.iter()
.filter(|b| scope.wants(&b.conv))
.cloned()
.collect()
})
.unwrap_or_default();
let transcript = crate::transcript::build(workspace, agent);
Observed {
goal: goal(workspace, agent),
governing: governing(snap, workspace, agent),
usage: total(&bills),
wall_secs: wall(&bills),
steps: bills.len(),
response: response(&transcript),
verdicts: verdicts(&transcript),
compacted: compacted(&transcript),
}
}
fn goal(workspace: &Path, agent: &str) -> Option<String> {
let path = workspace.join(AGENTS_DIR).join(agent).join(GOAL_FILE);
std::fs::read_to_string(path).ok()
}
fn governing(snap: &Snapshot, workspace: &Path, agent: &str) -> Option<String> {
let tip = snap
.trees
.get(workspace)?
.agents
.iter()
.find(|a| a.agent_id == agent)?
.tip_oid
.clone();
crate::config_edit::branch::governing_config(workspace, &tip)
.ok()
.map(|gov| gov.oid)
}
fn response(transcript: &Transcript) -> Option<String> {
let text = transcript
.entries
.iter()
.rev()
.find_map(model_text)
.unwrap_or_default();
(!text.is_empty()).then_some(text)
}
fn model_text(entry: &Entry) -> Option<String> {
let EntryKind::Model { blocks, .. } = &entry.kind else {
return None;
};
let said: Vec<&str> = blocks
.iter()
.filter_map(|b| match b {
Block::Text(text) => Some(text.as_str()),
_ => None,
})
.collect();
Some(said.join("\n"))
}
fn verdicts(transcript: &Transcript) -> Vec<super::Verdict> {
transcript
.entries
.iter()
.filter_map(|entry| match &entry.kind {
EntryKind::Delivered { sender, body, .. } => Some(super::Verdict {
sender: sender.clone(),
body: body.clone(),
}),
_ => None,
})
.collect()
}
fn compacted(transcript: &Transcript) -> usize {
transcript
.entries
.iter()
.filter_map(|entry| match &entry.kind {
EntryKind::Compacted { first, last, .. } => {
Some(last.saturating_sub(*first).saturating_add(1))
}
_ => None,
})
.sum()
}