use std::path::{Path, PathBuf};
use trusty_mpm::core::agent_cost::{AgentCostConfig, BudgetStatus, evaluate_cost};
const MAX_TRANSCRIPT_TAIL: u64 = 64 * 1024;
const RETRY_TRANSCRIPT_TAIL: u64 = 1024 * 1024;
const EVAL_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(200);
pub(crate) fn resolve_agent_transcript(payload: &serde_json::Value) -> Option<PathBuf> {
let field = |k: &str| {
payload
.get(k)
.and_then(serde_json::Value::as_str)
.filter(|s| !s.is_empty())
};
if let Some(p) = field("agent_transcript_path") {
let path = PathBuf::from(p);
if path.is_file() {
return Some(path);
}
}
let parent = field("transcript_path")?;
let parent = Path::new(parent);
if parent
.parent()
.is_some_and(|d| d.file_name().is_some_and(|n| n == "subagents"))
&& parent.is_file()
{
return Some(parent.to_path_buf());
}
let agent_id = field("agent_id")?;
let stem = parent.file_stem()?;
let derived = parent
.parent()?
.join(stem)
.join("subagents")
.join(format!("agent-{agent_id}.jsonl"));
derived.is_file().then_some(derived)
}
pub(crate) async fn evaluate_agent_cost(
payload: &serde_json::Value,
config: &AgentCostConfig,
) -> (BudgetStatus, u64) {
if !config.enabled {
return (BudgetStatus::Ok, 0);
}
let Some(path) = resolve_agent_transcript(payload) else {
return (BudgetStatus::Ok, 0);
};
let Some(tokens) = tokio::time::timeout(EVAL_TIMEOUT, read_latest_context(&path))
.await
.ok()
.flatten()
else {
return (BudgetStatus::Ok, 0);
};
(evaluate_cost(tokens, config), tokens)
}
async fn read_latest_context(path: &Path) -> Option<u64> {
use trusty_mpm::core::agent_cost::latest_context_tokens;
for bytes in [MAX_TRANSCRIPT_TAIL, RETRY_TRANSCRIPT_TAIL] {
let jsonl = super::misc::read_transcript_tail(path, bytes).await?;
if let Some(tokens) = latest_context_tokens(&jsonl) {
return Some(tokens);
}
if (jsonl.len() as u64) < bytes {
return None;
}
}
None
}
pub(crate) fn is_persistence_escape(
tool_name: &str,
tool_input: Option<&serde_json::Value>,
) -> bool {
use trusty_mpm::core::agent_cost::is_persistence_tool;
if is_persistence_tool(tool_name) {
return true;
}
if tool_name != "Bash" {
return false;
}
let command = tool_input
.and_then(|v| v.get("command"))
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
super::pm_guard_bash::command_is_persistence_only(command)
}
pub(crate) fn claim_warn_notice(payload: &serde_json::Value) -> bool {
let Some(key) = warn_notice_key(payload) else {
return true;
};
let dir = std::env::temp_dir().join("trusty-mpm-agent-cost");
if std::fs::create_dir_all(&dir).is_err() {
return true;
}
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(dir.join(key))
{
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => false,
Err(_) => true,
}
}
fn warn_notice_key(payload: &serde_json::Value) -> Option<String> {
let field = |k: &str| {
payload
.get(k)
.and_then(serde_json::Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_string)
};
let raw = field("agent_id")
.or_else(|| {
resolve_agent_transcript(payload)
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))
})
.or_else(|| field("session_id"))?;
let key: String = raw
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
.take(64)
.collect();
(!key.is_empty()).then_some(key)
}
#[cfg(test)]
mod tests {
use super::*;
use trusty_mpm::core::agent_cost::stop_reason;
fn transcript_tree(dir: &Path, agent_id: &str, context_tokens: u64) -> (PathBuf, PathBuf) {
let parent = dir.join("5b4e60d2-d5d9-4ec5-927f-4fb9198a296d.jsonl");
std::fs::write(&parent, "{}\n").expect("write parent");
let sub_dir = dir
.join("5b4e60d2-d5d9-4ec5-927f-4fb9198a296d")
.join("subagents");
std::fs::create_dir_all(&sub_dir).expect("mkdir");
let sub = sub_dir.join(format!("agent-{agent_id}.jsonl"));
let line = serde_json::json!({
"type": "assistant",
"message": { "usage": {
"input_tokens": 8,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": context_tokens - 8,
"output_tokens": 100
}}
});
std::fs::write(&sub, format!("{line}\n")).expect("write sub");
(parent, sub)
}
#[test]
fn derives_the_subagent_path_from_agent_id() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, sub) = transcript_tree(tmp.path(), "a1d57cf5a7f59b877", 100_000);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "a1d57cf5a7f59b877",
});
assert_eq!(resolve_agent_transcript(&payload).as_ref(), Some(&sub));
}
#[test]
fn resolves_explicit_agent_transcript_path() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, sub) = transcript_tree(tmp.path(), "abc123", 100_000);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_transcript_path": sub.to_str().expect("utf8"),
"agent_id": "abc123",
});
assert_eq!(resolve_agent_transcript(&payload).as_ref(), Some(&sub));
}
#[test]
fn resolves_a_transcript_path_already_under_subagents() {
let tmp = crate::test_support::hermetic_temp_dir();
let (_, sub) = transcript_tree(tmp.path(), "abc123", 100_000);
let payload = serde_json::json!({
"transcript_path": sub.to_str().expect("utf8"),
});
assert_eq!(resolve_agent_transcript(&payload).as_ref(), Some(&sub));
}
#[test]
fn fails_open_without_any_transcript_field() {
for payload in [
serde_json::json!({}),
serde_json::json!({"tool_name": "Bash"}),
serde_json::json!({"transcript_path": ""}),
serde_json::json!({"transcript_path": "/tmp/nope/parent.jsonl"}),
serde_json::json!({
"transcript_path": "/tmp/nope/parent.jsonl",
"agent_id": "ghost",
}),
] {
assert_eq!(
resolve_agent_transcript(&payload),
None,
"expected fail-open for {payload}"
);
}
}
#[tokio::test]
async fn fails_open_when_the_transcript_is_missing() {
let payload = serde_json::json!({
"transcript_path": "/tmp/definitely-not-here/p.jsonl",
"agent_id": "ghost",
});
let (status, tokens) = evaluate_agent_cost(&payload, &AgentCostConfig::default()).await;
assert_eq!(status, BudgetStatus::Ok);
assert_eq!(tokens, 0);
}
fn opted_in_stop() -> AgentCostConfig {
AgentCostConfig {
enabled: true,
max_tokens: 400_000,
..Default::default()
}
}
#[tokio::test]
async fn reports_exceeded_for_an_over_ceiling_transcript() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, _) = transcript_tree(tmp.path(), "big", 622_200);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "big",
});
let (status, tokens) = evaluate_agent_cost(&payload, &opted_in_stop()).await;
assert_eq!(status, BudgetStatus::Exceeded);
assert_eq!(tokens, 622_200);
assert!(stop_reason(tokens, 400_000).contains("622200"));
}
#[tokio::test]
async fn default_config_only_warns_on_the_same_transcript() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, _) = transcript_tree(tmp.path(), "big", 622_200);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "big",
});
let (status, tokens) = evaluate_agent_cost(&payload, &AgentCostConfig::default()).await;
assert_eq!(status, BudgetStatus::Warning);
assert_eq!(tokens, 622_200);
}
#[tokio::test]
async fn respects_a_disabled_config() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, _) = transcript_tree(tmp.path(), "big", 900_000);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "big",
});
let disabled = AgentCostConfig {
enabled: false,
..Default::default()
};
assert_eq!(
evaluate_agent_cost(&payload, &disabled).await,
(BudgetStatus::Ok, 0)
);
}
#[tokio::test]
async fn allows_a_healthy_agent() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, _) = transcript_tree(tmp.path(), "small", 71_540);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "small",
});
let (status, tokens) = evaluate_agent_cost(&payload, &AgentCostConfig::default()).await;
assert_eq!(status, BudgetStatus::Ok);
assert_eq!(tokens, 71_540);
}
fn filler_line(bytes: usize) -> String {
format!(
"{}\n",
serde_json::json!({"type": "user", "pad": "x".repeat(bytes)})
)
}
#[tokio::test]
async fn retries_with_a_larger_tail_when_64k_holds_no_usage_record() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, sub) = transcript_tree(tmp.path(), "huge", 500_000);
let mut jsonl = std::fs::read_to_string(&sub).expect("read");
jsonl.push_str(&filler_line(100 * 1024));
std::fs::write(&sub, &jsonl).expect("write");
assert!(
jsonl.len() as u64 > MAX_TRANSCRIPT_TAIL,
"the fixture must actually exceed the first window"
);
let first = super::super::misc::read_transcript_tail(&sub, MAX_TRANSCRIPT_TAIL)
.await
.expect("tail read");
assert_eq!(
trusty_mpm::core::agent_cost::latest_context_tokens(&first),
None,
"fixture invalid: 64 KiB must NOT contain a usage record"
);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "huge",
});
let (status, tokens) = evaluate_agent_cost(&payload, &opted_in_stop()).await;
assert_eq!(tokens, 500_000, "the larger window must find the record");
assert_eq!(status, BudgetStatus::Exceeded);
}
#[tokio::test]
async fn still_fails_open_when_even_the_larger_tail_has_no_record() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, sub) = transcript_tree(tmp.path(), "norec", 500_000);
std::fs::write(&sub, filler_line(100 * 1024)).expect("write");
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "norec",
});
assert_eq!(
evaluate_agent_cost(&payload, &opted_in_stop()).await,
(BudgetStatus::Ok, 0)
);
}
#[test]
fn escape_hatch_permits_send_message_and_git_persistence() {
assert!(is_persistence_escape("SendMessage", None));
for command in [
"git add -A",
"git commit -m 'fix: the thing'",
"git push origin HEAD",
"git -C /repo/wt commit -m x",
"git add -A && git commit -m x && git push",
] {
assert!(
is_persistence_escape("Bash", Some(&serde_json::json!({"command": command}))),
"a stopped agent must still be able to run {command:?}"
);
}
}
#[test]
fn escape_hatch_denies_work_tools() {
for tool in ["Write", "Edit", "Read", "Grep", "WebFetch", "Task", "Agent"] {
assert!(
!is_persistence_escape(tool, Some(&serde_json::json!({}))),
"{tool} must stay denied past the ceiling"
);
}
assert!(!is_persistence_escape("Bash", None));
for command in [
"cargo test",
"git commit -m x && cargo test",
"git checkout main",
] {
assert!(
!is_persistence_escape("Bash", Some(&serde_json::json!({"command": command}))),
"{command:?} must stay denied past the ceiling"
);
}
}
#[test]
fn escape_hatch_denies_exec_capable_git() {
for command in [
"git -c diff.external='cargo test' diff",
"git -c protocol.ext.allow=always push ext::sh -c 'cargo test' HEAD",
"git push --receive-pack='cargo test' /tmp/repo HEAD",
"git diff <(cargo test)",
] {
assert!(
!is_persistence_escape("Bash", Some(&serde_json::json!({"command": command}))),
"{command:?} executes a program and must not count as persistence"
);
}
}
#[test]
fn warn_notice_is_claimed_per_sibling_not_per_parent_session() {
let tmp = crate::test_support::hermetic_temp_dir();
let session = format!("parent-{}", std::process::id());
let ids = [
format!("sib-a-{}", std::process::id()),
format!("sib-b-{}", std::process::id()),
];
let subs: Vec<PathBuf> = ids
.iter()
.map(|id| transcript_tree(tmp.path(), id, 300_000).1)
.collect();
let markers: Vec<PathBuf> = ids
.iter()
.map(|id| {
std::env::temp_dir()
.join("trusty-mpm-agent-cost")
.join(format!("agent-{id}"))
})
.collect();
for m in &markers {
let _ = std::fs::remove_file(m);
}
for sub in &subs {
let payload = serde_json::json!({
"transcript_path": sub.to_str().expect("utf8"),
"session_id": session,
});
assert!(
claim_warn_notice(&payload),
"each sibling must get its own notice, not share the parent's"
);
assert!(!claim_warn_notice(&payload), "…and only one each");
}
for m in &markers {
let _ = std::fs::remove_file(m);
}
}
#[test]
fn warn_notice_fails_open_when_no_key_can_be_derived() {
for payload in [
serde_json::json!({"agent_id": "!!!"}),
serde_json::json!({"agent_id": "", "session_id": ""}),
serde_json::json!({}),
] {
assert!(warn_notice_key(&payload).is_none(), "{payload}");
for _ in 0..3 {
assert!(
claim_warn_notice(&payload),
"an underivable key must keep informing the agent: {payload}"
);
}
}
}
#[test]
fn warn_notice_is_claimed_once_per_agent() {
let id = format!("test-{}", std::process::id());
let payload = serde_json::json!({"agent_id": id});
let marker = std::env::temp_dir().join("trusty-mpm-agent-cost").join(&id);
let _ = std::fs::remove_file(&marker);
assert!(claim_warn_notice(&payload), "first call must claim");
assert!(!claim_warn_notice(&payload), "second call must not");
assert!(!claim_warn_notice(&payload), "and neither must the third");
let other = serde_json::json!({"agent_id": format!("{id}-other")});
assert!(claim_warn_notice(&other));
let _ = std::fs::remove_file(&marker);
let _ = std::fs::remove_file(
std::env::temp_dir()
.join("trusty-mpm-agent-cost")
.join(format!("{id}-other")),
);
}
}