use serde::{Deserialize, Serialize};
pub use crate::core::budget::BudgetStatus;
pub const DEFAULT_WARN_TOKENS: u64 = 250_000;
pub const DEFAULT_MAX_TOKENS: u64 = 0;
pub const PERSISTENCE_GIT_SUBCOMMANDS: &[&str] = &["add", "commit", "push", "status", "diff"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct AgentCostConfig {
pub enabled: bool,
pub warn_tokens: u64,
pub max_tokens: u64,
}
impl Default for AgentCostConfig {
fn default() -> Self {
Self {
enabled: true,
warn_tokens: DEFAULT_WARN_TOKENS,
max_tokens: DEFAULT_MAX_TOKENS,
}
}
}
pub fn evaluate_cost(context_tokens: u64, config: &AgentCostConfig) -> BudgetStatus {
if !config.enabled {
return BudgetStatus::Ok;
}
if config.max_tokens > 0 && context_tokens >= config.max_tokens {
return BudgetStatus::Exceeded;
}
if config.warn_tokens > 0 && context_tokens >= config.warn_tokens {
return BudgetStatus::Warning;
}
BudgetStatus::Ok
}
pub fn latest_context_tokens(jsonl: &str) -> Option<u64> {
fn context_of(usage: &serde_json::Value) -> Option<u64> {
let field = |k: &str| usage.get(k).and_then(serde_json::Value::as_u64);
let parts = [
field("input_tokens"),
field("cache_creation_input_tokens"),
field("cache_read_input_tokens"),
];
parts
.iter()
.any(Option::is_some)
.then(|| parts.iter().flatten().sum())
}
jsonl
.lines()
.rev()
.filter_map(|line| {
let value: serde_json::Value = serde_json::from_str(line).ok()?;
let usage = value.get("message")?.get("usage")?;
context_of(usage)
})
.next()
}
pub fn is_persistence_tool(tool_name: &str) -> bool {
tool_name == "SendMessage"
}
pub fn stop_reason(context_tokens: u64, max_tokens: u64) -> String {
let git = PERSISTENCE_GIT_SUBCOMMANDS.join(", git ");
format!(
"Agent context ceiling reached (#4837): this subagent is carrying {context_tokens} tokens \
of context, at or past the {max_tokens}-token cap. Every further tool call re-sends all \
of it, so continuing costs more per step than starting over. SAVE AND REPORT BACK NOW. \
Still permitted so you never lose work: SendMessage, and Bash running only git {git} \
(every segment of a composed command must be one of those). Those git commands must be \
plain: no shell metacharacters — $, backtick, ( ) < > — outside single quotes, so write \
git commit -m 'literal text' and never -m \"fix $ISSUE\"; no :: in any argument, even \
inside a quoted message, because git reads :: as a remote-helper transport, so reword a \
commit message that would say std::process::Command; and no exotic long flags, only the \
ordinary ones like -m, -A, -u, --amend, --force-with-lease, --stat, --short. \
Everything else — including Write, Edit, Read, and any other Bash — is denied until the \
PM re-dispatches you. Commit and push what you have, then SendMessage the PM: what you \
completed, what remains, and what a fresh agent needs to finish it. To raise the ceiling \
instead, set agent_cost.max_tokens in ~/.trusty-mpm/config.toml."
)
}
pub fn warn_reason(context_tokens: u64, config: &AgentCostConfig) -> String {
let stop = if config.max_tokens > 0 {
format!(
" Your next tool call is DENIED once you reach {}.",
config.max_tokens
)
} else {
String::new()
};
format!(
"Context-cost notice (#4837): you are carrying {context_tokens} tokens of context, past \
the {} warn threshold. Every tool call from here re-sends all of it, so each further \
step costs more than the last.{stop} Wrap up: finish or checkpoint what you are on, \
commit and push it, and report back to the PM with what remains so a fresh agent can \
take it. Do not start new lines of investigation.",
config.warn_tokens
)
}
#[cfg(test)]
mod tests {
use super::*;
const EVIDENCE_OVERRUNS: [u64; 3] = [413_300, 418_500, 622_200];
const EVIDENCE_MIDLIFE: [u64; 2] = [255_500, 269_900];
fn opted_in_stop() -> AgentCostConfig {
AgentCostConfig {
enabled: true,
warn_tokens: DEFAULT_WARN_TOKENS,
max_tokens: 400_000,
}
}
#[test]
fn defaults_are_warn_only() {
let cfg = AgentCostConfig::default();
assert!(
cfg.enabled,
"the warn level is on by default — that is what ships"
);
assert_eq!(cfg.warn_tokens, 250_000);
assert_eq!(
cfg.max_tokens, 0,
"the hard stop must default to 0 (opt-in), not 400_000"
);
assert_eq!(evaluate_cost(136_000, &cfg), BudgetStatus::Ok, "p50");
assert_eq!(evaluate_cost(268_000, &cfg), BudgetStatus::Warning, "p90");
}
#[test]
fn default_config_never_stops_any_evidence_row() {
let cfg = AgentCostConfig::default();
for tokens in EVIDENCE_OVERRUNS
.into_iter()
.chain(EVIDENCE_MIDLIFE)
.chain([u64::MAX])
{
assert_eq!(
evaluate_cost(tokens, &cfg),
BudgetStatus::Warning,
"{tokens} must warn, never stop, under the shipped default"
);
}
}
#[test]
fn stops_every_overrun_once_the_operator_opts_in() {
let cfg = opted_in_stop();
for tokens in EVIDENCE_OVERRUNS {
assert_eq!(
evaluate_cost(tokens, &cfg),
BudgetStatus::Exceeded,
"{tokens} tokens must be stopped under an opted-in ceiling"
);
}
for tokens in EVIDENCE_MIDLIFE {
assert_eq!(evaluate_cost(tokens, &cfg), BudgetStatus::Warning);
}
assert_eq!(evaluate_cost(136_000, &cfg), BudgetStatus::Ok);
}
#[test]
fn warns_at_the_warn_threshold() {
let cfg = AgentCostConfig::default();
assert_eq!(evaluate_cost(249_999, &cfg), BudgetStatus::Ok);
assert_eq!(evaluate_cost(250_000, &cfg), BudgetStatus::Warning);
}
#[test]
fn stops_at_the_max_threshold() {
let cfg = opted_in_stop();
assert_eq!(evaluate_cost(399_999, &cfg), BudgetStatus::Warning);
assert_eq!(evaluate_cost(400_000, &cfg), BudgetStatus::Exceeded);
assert_eq!(evaluate_cost(u64::MAX, &cfg), BudgetStatus::Exceeded);
}
#[test]
fn disabled_config_always_allows() {
let cfg = AgentCostConfig {
enabled: false,
..Default::default()
};
for tokens in [0, 250_000, 400_000, u64::MAX] {
assert_eq!(evaluate_cost(tokens, &cfg), BudgetStatus::Ok);
}
}
#[test]
fn zero_max_is_unlimited() {
let cfg = AgentCostConfig {
enabled: true,
warn_tokens: 0,
max_tokens: 0,
};
assert_eq!(evaluate_cost(u64::MAX, &cfg), BudgetStatus::Ok);
let stop_only = AgentCostConfig {
enabled: true,
warn_tokens: 0,
max_tokens: 400_000,
};
assert_eq!(evaluate_cost(300_000, &stop_only), BudgetStatus::Ok);
assert_eq!(evaluate_cost(400_000, &stop_only), BudgetStatus::Exceeded);
}
#[test]
fn config_override_changes_thresholds() {
let cfg = AgentCostConfig {
enabled: true,
warn_tokens: 700_000,
max_tokens: 900_000,
};
for tokens in EVIDENCE_OVERRUNS {
assert_eq!(
evaluate_cost(tokens, &cfg),
BudgetStatus::Ok,
"{tokens} must pass under a widened ceiling"
);
}
assert_eq!(evaluate_cost(700_000, &cfg), BudgetStatus::Warning);
assert_eq!(evaluate_cost(900_000, &cfg), BudgetStatus::Exceeded);
}
#[test]
fn misordered_thresholds_still_stop() {
let cfg = AgentCostConfig {
enabled: true,
warn_tokens: 900_000,
max_tokens: 100_000,
};
assert_eq!(evaluate_cost(150_000, &cfg), BudgetStatus::Exceeded);
}
#[test]
fn config_section_parses_from_toml() {
let cfg: AgentCostConfig =
toml::from_str("enabled = true\nmax_tokens = 123456\n").expect("parses");
assert!(cfg.enabled);
assert_eq!(cfg.max_tokens, 123_456);
assert_eq!(cfg.warn_tokens, DEFAULT_WARN_TOKENS);
}
fn assistant_line(input: u64, cache_creation: u64, cache_read: u64) -> String {
serde_json::json!({
"type": "assistant",
"message": {
"role": "assistant",
"usage": {
"input_tokens": input,
"cache_creation_input_tokens": cache_creation,
"cache_read_input_tokens": cache_read,
"output_tokens": 873,
"service_tier": "standard"
}
}
})
.to_string()
}
#[test]
fn reads_context_tokens_from_the_last_usage_record() {
let jsonl = format!(
"{}\n{}\n",
assistant_line(8, 776, 70_756),
assistant_line(12, 1_000, 410_000)
);
assert_eq!(latest_context_tokens(&jsonl), Some(411_012));
}
#[test]
fn tolerates_a_truncated_leading_line() {
let jsonl = format!(
"ache_read_input_tokens\":70756}}}}}}\n{}\n",
assistant_line(8, 776, 300_000)
);
assert_eq!(latest_context_tokens(&jsonl), Some(300_784));
}
#[test]
fn returns_none_without_a_usage_record() {
let jsonl = "{\"type\":\"user\",\"message\":{\"role\":\"user\"}}\n";
assert_eq!(latest_context_tokens(jsonl), None);
assert_eq!(latest_context_tokens(""), None);
assert_eq!(latest_context_tokens("not json at all\n"), None);
}
#[test]
fn ignores_a_usage_block_with_no_token_fields() {
let bare = serde_json::json!({
"message": { "usage": { "output_tokens": 5 } }
})
.to_string();
assert_eq!(latest_context_tokens(&bare), None);
let jsonl = format!("{}\n{}\n", assistant_line(8, 776, 500_000), bare);
assert_eq!(latest_context_tokens(&jsonl), Some(500_784));
}
#[test]
fn persistence_tools_cover_report_and_commit() {
assert!(is_persistence_tool("SendMessage"));
for tool in [
"Write", "Edit", "Read", "Grep", "Glob", "Bash", "WebFetch", "Task", "Agent",
] {
assert!(
!is_persistence_tool(tool),
"{tool} must not be unconditionally permitted past the ceiling"
);
}
for sub in ["add", "commit", "push", "status", "diff"] {
assert!(PERSISTENCE_GIT_SUBCOMMANDS.contains(&sub), "git {sub}");
}
for sub in ["checkout", "reset", "rebase", "merge", "clean", "worktree"] {
assert!(
!PERSISTENCE_GIT_SUBCOMMANDS.contains(&sub),
"git {sub} is not persistence and must stay denied"
);
}
}
#[test]
fn stop_reason_names_the_numbers_and_the_escape_hatch() {
let reason = stop_reason(622_200, 400_000);
assert!(reason.contains("622200"));
assert!(reason.contains("400000"));
assert!(reason.contains("SendMessage"));
assert!(
!reason.contains("never blocked"),
"the deny text must not repeat the fan-out guard's blanket claim"
);
for sub in PERSISTENCE_GIT_SUBCOMMANDS {
assert!(reason.contains(sub), "deny text must name git {sub}");
}
assert!(reason.contains("Write, Edit"));
assert!(reason.contains("agent_cost.max_tokens"));
}
#[test]
fn stop_reason_names_the_metacharacter_rule() {
let reason = stop_reason(622_200, 400_000);
assert!(
reason.contains("metacharacter"),
"the deny text must name the rule that rejects `-m \"fix $ISSUE\"`"
);
assert!(
reason.contains("single quotes"),
"naming the rule without the fix still costs a retry"
);
for ch in ["$", "backtick", "(", ")", "<", ">"] {
assert!(
reason.contains(ch),
"the deny text must name the rejected character {ch:?}"
);
}
assert!(reason.contains("--force-with-lease"));
}
#[test]
fn stop_reason_names_the_double_colon_rule() {
let reason = stop_reason(622_200, 400_000);
assert!(
reason.contains("::"),
"the deny text must name the rule that rejects `-m 'use std::process::Command'`"
);
assert!(
reason.contains("quoted message"),
"naming `::` without saying quotes do not exempt it still costs a retry"
);
}
#[test]
fn warn_reason_addresses_the_agent() {
let text = warn_reason(268_000, &opted_in_stop());
assert!(text.contains("268000"));
assert!(text.contains("250000"));
assert!(text.contains("400000"), "a configured stop must be named");
assert!(text.contains("report back to the PM"));
}
#[test]
fn warn_reason_omits_a_stop_that_is_not_configured() {
let text = warn_reason(268_000, &AgentCostConfig::default());
assert!(text.contains("268000"));
assert!(
!text.contains("DENIED"),
"must not threaten a stop that will never fire: {text}"
);
}
}