use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use misanthropic::prompt::Prompt;
use misanthropic::tool::{self, MethodDef, Notifications, Tool, Use};
use crate::ids::{AgentId, ContentRef};
use crate::requests::GetContentInput;
pub const CONTEXT_BUFFER_TOKENS: u64 = 16_000;
pub const PHASE_PROMPT_TOKENS: u64 = 2_000;
pub const CLOSING_RESERVE_MIN: u64 = 32_768;
pub const CLOSING_RESERVE_DIVISOR: u64 = 4;
pub const MIN_ROUND_ROOM: u64 = 2_000;
#[derive(Debug, Clone)]
pub struct ContextGauge(Arc<Inner>);
#[derive(Debug)]
struct Inner {
held: AtomicU64,
window: AtomicU64,
refused: AtomicU64,
floor: u64,
}
impl Default for ContextGauge {
fn default() -> Self {
Self::new(super::DEFAULT_CONTEXT_WINDOW, CONTEXT_BUFFER_TOKENS)
}
}
impl ContextGauge {
pub fn new(window: u64, floor: u64) -> Self {
Self(Arc::new(Inner {
held: AtomicU64::new(0),
window: AtomicU64::new(window),
refused: AtomicU64::new(0),
floor,
}))
}
pub fn reserve_for(act_max_tokens: u32, phase_max_tokens: u32) -> u64 {
u64::from(act_max_tokens)
+ u64::from(phase_max_tokens)
+ PHASE_PROMPT_TOKENS
}
pub fn get(&self) -> u64 {
self.0.held.load(Ordering::Relaxed)
}
pub fn set(&self, tokens: u64) {
self.0.held.store(tokens, Ordering::Relaxed);
}
pub fn add(&self, tokens: u64) {
self.0.held.fetch_add(tokens, Ordering::Relaxed);
}
pub fn window(&self) -> u64 {
self.0.window.load(Ordering::Relaxed)
}
pub fn set_window(&self, window: u64) {
self.0.window.store(window, Ordering::Relaxed);
}
pub fn reserve(&self) -> u64 {
(self.window() / CLOSING_RESERVE_DIVISOR)
.max(CLOSING_RESERVE_MIN)
.max(self.0.floor)
}
pub fn fits(&self, tokens: u64) -> bool {
self.get() + tokens + self.reserve() <= self.window()
}
pub fn room(&self) -> u64 {
self.window()
.saturating_sub(self.get())
.saturating_sub(self.reserve())
}
pub fn phase_room(&self) -> u64 {
self.window()
.saturating_sub(self.get())
.saturating_sub(PHASE_PROMPT_TOKENS)
}
pub fn record(&self, usage: &misanthropic::response::Usage) {
let input = usage.input_tokens
+ usage.cache_read_input_tokens.unwrap_or(0)
+ usage.cache_creation_input_tokens.unwrap_or(0);
self.set(input + usage.output_tokens);
self.0.refused.store(0, Ordering::Relaxed);
}
fn refuse(&self) -> u64 {
self.0.refused.fetch_add(1, Ordering::Relaxed)
}
}
pub const BYTES_PER_TOKEN: u64 = 3;
pub(super) fn estimate_tokens(text: &str) -> u64 {
text.len() as u64 / BYTES_PER_TOKEN
}
pub struct Gauged<T> {
inner: T,
context: ContextGauge,
agent_id: Option<AgentId>,
}
impl<T> Gauged<T> {
pub fn new(inner: T, context: ContextGauge) -> Self {
Self {
inner,
context,
agent_id: None,
}
}
pub fn for_agent(mut self, agent_id: AgentId) -> Self {
self.agent_id = Some(agent_id);
self
}
fn admit(
&self,
call: &str,
input: &serde_json::Value,
mut result: tool::Result,
) -> tool::Result {
let text = result.content.to_string();
let tokens = estimate_tokens(&text);
if self.context.fits(tokens) {
self.context.add(tokens);
return result;
}
let held = self.context.get();
let room = self.context.room();
let requested = Requested::of(call, input);
tracing::warn!(
agent_id = self.agent_id.as_ref().map(tracing::field::display),
tool = call,
target = requested.target.as_ref().map(tracing::field::display),
comment_budget = requested.comment_budget,
tokens,
held,
room,
window = self.context.window(),
"tool result would not fit in context; replaced by a note"
);
let note = if self.context.refuse() == 0 {
format!(
"This result is about {tokens} tokens ({} KB); with about {held} \
already in your context (this turn's other results included), \
it would not fit in your {} token window less the {} kept for \
the end of your session, so it was left out. About {room} \
tokens are left for results this turn. {}",
text.len() / 1024,
self.context.window(),
self.context.reserve(),
instead(call, room, &text, &requested),
)
} else {
format!(
"Left out too: about {tokens} tokens ({} KB), which would not \
fit either (see the first note this turn).",
text.len() / 1024
)
};
self.context.add(estimate_tokens(¬e));
result.content = note.into();
result.is_error = true;
result
}
}
#[derive(Debug, Default)]
struct Requested {
target: Option<ContentRef>,
comment_budget: Option<u32>,
}
impl Requested {
fn of(call: &str, input: &serde_json::Value) -> Self {
if call != "get_content" {
return Self::default();
}
serde_json::from_value::<GetContentInput>(input.clone())
.map(|i| Self {
target: Some(i.id),
comment_budget: i.comment_budget,
})
.unwrap_or_default()
}
}
fn instead(call: &str, room: u64, text: &str, requested: &Requested) -> String {
let fallbacks = |budget: bool| {
format!(
"Read the post with detail=\"summary\" (without its comments), or \
read one comment by its id (it comes with the post and up to {} \
of the comments it replies to){}. For a data set's file (DATA-…, \
attachment=<path>) or listing, a smaller comment_budget shows \
less of it, and the cut says how much.",
GetContentInput::ANCESTOR_LIMIT,
if budget {
", or use the smaller comment_budget above"
} else {
""
}
)
};
if call != "get_content" {
return match call {
"search" | "get_feed" | "get_governance_log" | "get_proposals" => {
"Ask for fewer with a smaller limit.".into()
}
_ => "Ask for something smaller.".into(),
};
}
let min = u64::from(GetContentInput::MIN_COMMENT_BUDGET);
let max = u64::from(GetContentInput::MAX_COMMENT_BUDGET);
if !matches!(
requested.target,
Some(ContentRef::Content(_) | ContentRef::ContentPrefix(_))
) {
return format!(
"For a Council decision, read one round (round=<n>) or \
detail=\"summary\"; for a governance entry's attachments, one \
at a time (attachment=<name>). {}",
fallbacks(false)
);
}
let Some(fixed) = super::prompt::unbudgeted_bytes(text) else {
return format!(
"A comment_budget does not shrink this read. {}",
fallbacks(false)
);
};
let fixed = fixed as u64 / BYTES_PER_TOKEN;
let usable = room.saturating_sub(fixed).saturating_mul(9) / 10;
let bytes = usable.saturating_mul(BYTES_PER_TOKEN).min(max);
if bytes < min {
format!(
"Even the smallest comment_budget ({min}) would not make this read \
fit: about {fixed} tokens of it (the post and each stub's line) \
come with any budget. {}",
fallbacks(false)
)
} else {
format!(
"A comment_budget of at most {bytes} would fit: about {fixed} \
tokens of this read (the post and each stub's line) come with any \
budget. {}",
fallbacks(true)
)
}
}
#[async_trait::async_trait]
impl<T: Tool> Tool for Gauged<T> {
fn name(&self) -> &str {
self.inner.name()
}
fn definitions(&self) -> Vec<MethodDef> {
self.inner.definitions()
}
async fn call(&mut self, call: Use) -> tool::Result {
let name = call.name.clone();
let input = call.input.clone();
let result = self.inner.call(call).await;
self.admit(&name, &input, result)
}
async fn save_json(&mut self) -> serde_json::Value {
self.inner.save_json().await
}
async fn load_json(
&mut self,
json: serde_json::Value,
) -> Result<(), String> {
self.inner.load_json(json).await
}
fn connect(&mut self, mailbox: tool::Mailbox) {
self.inner.connect(mailbox)
}
async fn on_init(
&mut self,
prompt: &mut Prompt,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.inner.on_init(prompt).await
}
async fn on_turn(
&mut self,
prompt: &mut Prompt,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.inner.on_turn(prompt).await
}
async fn on_teardown(
&mut self,
prompt: &mut Prompt,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.inner.on_teardown(prompt).await
}
fn subscribe(&mut self) -> Option<Notifications> {
self.inner.subscribe()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_gauge_counts_cached_input_and_output() {
let gauge = ContextGauge::default();
let usage: misanthropic::response::Usage = serde_json::from_str(
r#"{
"input_tokens": 10,
"cache_read_input_tokens": 90000,
"cache_creation_input_tokens": 1000,
"output_tokens": 500
}"#,
)
.unwrap();
gauge.record(&usage);
assert_eq!(gauge.get(), 91_510);
gauge.add(10);
assert_eq!(gauge.clone().get(), 91_520, "shared, not copied");
}
struct Echo {
len: usize,
}
#[async_trait::async_trait]
impl Tool for Echo {
fn name(&self) -> &str {
"echo"
}
fn definitions(&self) -> Vec<MethodDef> {
Vec::new()
}
async fn call(&mut self, call: Use) -> tool::Result {
tool::Result::new(call.id, "x".repeat(self.len))
}
}
fn call(name: &str) -> Use {
serde_json::from_str(&format!(
r#"{{"id": "toolu_1", "name": "{name}", "input": {{}}}}"#
))
.unwrap()
}
#[tokio::test]
async fn every_result_is_counted_and_an_oversized_one_left_out() {
let gauge = ContextGauge::new(60_000, CONTEXT_BUFFER_TOKENS);
gauge.set(10_000);
let mut tool = Gauged::new(Echo { len: 30_000 }, gauge.clone());
let first = tool.call(call("search")).await;
assert!(!first.is_error);
assert_eq!(first.content.to_string().len(), 30_000);
assert_eq!(gauge.get(), 20_000);
let second = tool.call(call("search")).await;
assert!(second.is_error);
assert_eq!(second.tool_use_id, "toolu_1", "still answers the call");
let note = second.content.to_string();
assert!(note.contains("about 10000 tokens (29 KB)"), "{note}");
assert!(note.contains("about 20000 already"), "{note}");
assert!(note.contains("less the 32768 kept for the end"), "{note}");
assert!(note.contains("smaller limit"), "{note}");
assert!(gauge.get() > 20_000 && gauge.get() < 20_200, "the note");
}
#[tokio::test]
async fn a_wide_turn_of_refusals_leaves_the_session_its_end() {
let (act, phase) = (16_384u32, 16_384u32);
let gauge =
ContextGauge::new(131_072, ContextGauge::reserve_for(act, phase));
assert_eq!(gauge.reserve(), 34_768);
gauge.set(131_072 - 34_768 - MIN_ROUND_ROOM);
assert_eq!(gauge.room(), MIN_ROUND_ROOM);
let mut tool = Gauged::new(Echo { len: 60_000 }, gauge.clone());
let mut notes = Vec::new();
for _ in 0..100 {
let r = tool.call(call("get_content")).await;
assert!(r.is_error);
notes.push(r.content.to_string());
}
assert!(notes[0].contains("would not fit in your 131072 token window"));
assert!(
notes[1].starts_with("Left out too: about 20000 tokens"),
"{}",
notes[1]
);
assert!(
gauge.get() + u64::from(act) <= gauge.window(),
"{}",
gauge.get()
);
let reflect =
gauge.window() - gauge.get() - u64::from(act) - PHASE_PROMPT_TOKENS;
assert!(reflect >= 12_000, "reflect keeps {reflect} of {phase}");
gauge.record(&misanthropic::response::Usage::default());
gauge.set(131_072 - 34_768 - MIN_ROUND_ROOM);
let r = tool.call(call("get_content")).await;
assert!(r.content.to_string().starts_with("This result is about"));
}
fn call_with(name: &str, input: serde_json::Value) -> Use {
serde_json::from_value(serde_json::json!({
"id": "toolu_1", "name": name, "input": input
}))
.unwrap()
}
#[tokio::test]
async fn a_refusal_says_what_is_left_and_only_a_budget_that_fits() {
let post =
|| call_with("get_content", serde_json::json!({"id": "05676b9d"}));
let gauge = ContextGauge::new(60_000, CONTEXT_BUFFER_TOKENS);
gauge.set(10_000);
let mut tool = Gauged::new(Echo { len: 60_000 }, gauge.clone());
let note = tool.call(post()).await.content.to_string();
assert!(
note.contains("About 17232 tokens are left for results this turn."),
"{note}"
);
assert!(
note.contains("A comment_budget does not shrink this read."),
"{note}"
);
assert!(!note.contains("would fit:"), "{note}");
assert!(!note.contains("use the smaller comment_budget"), "{note}");
let text = post_read(2, 40);
let fixed = super::super::prompt::unbudgeted_bytes(&text).unwrap()
as u64
/ BYTES_PER_TOKEN;
let tight = ContextGauge::new(131_072, CONTEXT_BUFFER_TOKENS);
tight.set(tight.window() - tight.reserve() - (fixed - 1_000));
let mut tool = Gauged::new(Fixed { text }, tight.clone());
let note = tool.call(post()).await.content.to_string();
assert!(
note.contains("Even the smallest comment_budget (4096) would not make this read fit"),
"{note}"
);
assert!(!note.contains("would fit:"), "{note}");
assert!(!note.contains("use the smaller comment_budget"), "{note}");
let gov = ContextGauge::new(60_000, CONTEXT_BUFFER_TOKENS);
gov.set(10_000);
let mut tool = Gauged::new(Echo { len: 60_000 }, gov.clone());
let note = tool
.call(call_with(
"get_content",
serde_json::json!({"id": "GOV-2026-0006"}),
))
.await
.content
.to_string();
assert!(note.contains("read one round (round=<n>)"), "{note}");
assert!(!note.contains("would fit:"), "{note}");
let other = ContextGauge::new(60_000, CONTEXT_BUFFER_TOKENS);
other.set(20_000);
let mut tool = Gauged::new(Echo { len: 60_000 }, other.clone());
let note = tool.call(call("search")).await.content.to_string();
assert!(note.contains("About 7232 tokens are left"), "{note}");
assert!(
note.contains("Ask for fewer with a smaller limit."),
"{note}"
);
assert!(!note.contains("comment_budget"), "{note}");
}
fn post_read(full: usize, stubs: usize) -> String {
let at =
|i: usize| format!("2026-10-16T{:02}:{:02}:00Z", i / 60, i % 60);
let comments: Vec<_> = (0..full)
.map(|i| {
serde_json::json!({
"id": uuid::Uuid::new_v4(),
"post_id": uuid::Uuid::new_v4(),
"agent_id": uuid::Uuid::new_v4(),
"agent_name": "engineer",
"body": "y".repeat(3_000),
"created_at": at(i),
})
})
.collect();
let comment_stubs: Vec<_> = (0..stubs)
.map(|i| {
serde_json::json!({
"id": uuid::Uuid::new_v4(),
"agent_name": "lawyer",
"preview": "z".repeat(100),
"reply_count": 0,
"bytes": 2_000,
"created_at": at(600 + i),
})
})
.collect();
let post: crate::responses::PostWithCommentsResponse =
serde_json::from_value(serde_json::json!({
"post": {
"id": uuid::Uuid::new_v4(),
"agent_id": uuid::Uuid::new_v4(),
"agent_name": "author",
"community_id": uuid::Uuid::new_v4(),
"community_name": "tech",
"title": "A long thread",
"body": "x".repeat(36_000),
"comment_count": full + stubs,
},
"comments": comments,
"comment_stubs": comment_stubs,
"omitted_comment_count": stubs,
}))
.expect("fixture");
super::super::prompt::format_post(&post, &"reader".parse().unwrap())
}
struct Fixed {
text: String,
}
#[async_trait::async_trait]
impl Tool for Fixed {
fn name(&self) -> &str {
"get_content"
}
fn definitions(&self) -> Vec<MethodDef> {
Vec::new()
}
async fn call(&mut self, call: Use) -> tool::Result {
tool::Result::new(call.id, self.text.clone())
}
}
#[tokio::test]
async fn a_refused_post_read_measures_what_no_budget_shrinks() {
let text = post_read(30, 40);
let fixed = super::super::prompt::unbudgeted_bytes(&text).unwrap()
as u64
/ BYTES_PER_TOKEN;
assert!((12_000..18_000).contains(&fixed), "{fixed}");
assert_eq!(estimate_tokens(&text).saturating_sub(262_144 / 3), 0);
let gauge = ContextGauge::new(131_072, CONTEXT_BUFFER_TOKENS);
let room = fixed + 20_000;
gauge.set(gauge.window() - gauge.reserve() - room);
assert!(!gauge.fits(estimate_tokens(&text)), "refused");
let mut tool = Gauged::new(Fixed { text }, gauge.clone());
let note = tool
.call(call_with(
"get_content",
serde_json::json!({"id": "05676b9d", "comment_budget": "262144"}),
))
.await
.content
.to_string();
let budget = 20_000 * 9 / 10 * BYTES_PER_TOKEN;
assert!(
note.contains(&format!(
"A comment_budget of at most {budget} would fit: about {fixed} \
tokens of this read (the post and each stub's line) come \
with any budget."
)),
"{note}"
);
let admitted = budget as usize
/ (3_000 + crate::threads::COMMENT_LINE_BYTES + "engineer".len());
let again = post_read(admitted, 40 + 30 - admitted);
assert!(
estimate_tokens(&again) <= room,
"{} > {room}",
estimate_tokens(&again)
);
}
#[test]
fn a_refused_reads_target_and_budget_are_parsed_for_the_log() {
let r = Requested::of(
"get_content",
&serde_json::json!({"id": "05676b9d", "comment_budget": "262144"}),
);
assert_eq!(r.target.unwrap().to_string(), "05676b9d");
assert_eq!(r.comment_budget, Some(262_144));
let r = Requested::of(
"get_content",
&serde_json::json!({"id": "protocol"}),
);
assert_eq!(r.target.unwrap().to_string(), "protocol");
assert_eq!(r.comment_budget, None);
let r = Requested::of("search", &serde_json::json!({"query": "x"}));
assert!(r.target.is_none() && r.comment_budget.is_none());
}
#[test]
fn the_reserve_is_a_quarter_of_the_window_with_a_floor() {
let gauge = ContextGauge::new(262_144, 10_000);
assert_eq!(gauge.reserve(), 65_536);
gauge.set_window(131_072);
assert_eq!(gauge.reserve(), 32_768);
gauge.set_window(64_000);
assert_eq!(gauge.reserve(), CLOSING_RESERVE_MIN);
let budgets = ContextGauge::new(64_000, 40_000);
assert_eq!(budgets.reserve(), 40_000);
}
}