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 use crate::threads::BYTES_PER_TOKEN;
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>,
shown_once: Option<super::ShownOnce>,
}
impl<T> Gauged<T> {
pub fn new(inner: T, context: ContextGauge) -> Self {
Self {
inner,
context,
agent_id: None,
shown_once: None,
}
}
pub fn with_shown_once(mut self, shown_once: super::ShownOnce) -> Self {
self.shown_once = Some(shown_once);
self
}
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);
if let Some(once) = &self.shown_once {
once.commit();
}
return result;
}
if let Some(once) = &self.shown_once {
once.discard();
}
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 ~{tokens} tok; with ~{held} tok 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. {}",
self.context.window(),
self.context.reserve(),
requested.advice(call, room, &text),
)
} else {
format!(
"Left out too: ~{tokens} tok, which would not fit either \
(see the first note this turn)."
)
};
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>,
attachment: Option<String>,
lines: Option<crate::govlog::LineRange>,
link: Option<(crate::links::AgoraLink, &'static str)>,
}
impl Requested {
fn is_data_file(&self) -> bool {
match &self.target {
Some(ContentRef::DataCitation(_)) => true,
Some(r @ ContentRef::Governance(_)) => {
r.data_set().is_some()
&& self
.attachment
.as_deref()
.is_some_and(|a| !a.ends_with('/'))
}
_ => false,
}
}
fn first_line(&self) -> u64 {
self.lines
.or_else(|| {
self.target
.as_ref()
.and_then(ContentRef::as_data_citation)
.and_then(|c| c.lines)
})
.map_or(1, |r| r.start())
}
}
impl Requested {
fn of(call: &str, input: &serde_json::Value) -> Self {
let read = |i: GetContentInput| Self {
target: Some(i.id),
comment_budget: i.comment_budget,
attachment: i.attachment,
lines: i.lines,
link: None,
};
match call {
"get_content" => {
serde_json::from_value::<GetContentInput>(input.clone())
.map(read)
.unwrap_or_default()
}
"fetch_agora_link" => serde_json::from_value::<
crate::requests::FetchAgoraLinkInput,
>(input.clone())
.map(|i| {
let tool = i.link.tool();
Self {
link: Some((i.link.clone(), tool)),
..i.link.as_read().cloned().map(read).unwrap_or_default()
}
})
.unwrap_or_default(),
_ => Self::default(),
}
}
fn tool<'a>(&self, call: &'a str) -> &'a str {
self.link.as_ref().map_or(call, |(_, tool)| tool)
}
fn advice(&self, call: &str, room: u64, text: &str) -> String {
let tool = self.tool(call);
let advice = instead(tool, room, text, self);
if self.link.is_some() && tool == "get_content" && !self.is_data_file()
{
format!("{advice} {LINK_OPTIONS}")
} else {
advice
}
}
}
const LINK_OPTIONS: &str = "With fetch_agora_link these are options in the \
link (…?detail=summary, …?comment_budget=<bytes>, …?round=<n>), and an \
attachment is the last part of its path.";
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" && requested.is_data_file() {
return data_file_instead(room, text, requested);
}
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)
)
}
}
fn data_file_instead(room: u64, text: &str, requested: &Requested) -> String {
const HEADER_BYTES: u64 = 1_024;
let usable = (room.saturating_mul(9) / 10)
.saturating_mul(BYTES_PER_TOKEN)
.saturating_sub(HEADER_BYTES);
let lines = (text.matches('\n').count() as u64).max(1);
let per_line = (text.len() as u64 / lines).max(1);
let fit = usable / per_line;
if fit == 0 {
return "Not even a few lines of this file fit the room left this turn: \
read it in a later turn, when your context has more room."
.into();
}
let first = requested.first_line();
let last = first + fit - 1;
if let Some(read) =
requested.link.as_ref().and_then(|(link, _)| link.as_read())
&& let Ok(cut) = crate::govlog::LineRange::new(first, Some(last))
{
let mut read = read.clone();
match &mut read.id {
ContentRef::DataCitation(c) if c.lines.is_some() => {
c.lines = Some(cut)
}
_ => read.lines = Some(cut),
}
return format!(
"Read it whole in a fresh turn ({tokens}), or \
link=\"{link}\" now (about {fit} lines).",
tokens = crate::threads::about_tokens(text.len() as u64),
link = crate::links::AgoraLink::from(read),
);
}
let range = match requested
.target
.as_ref()
.and_then(ContentRef::as_data_citation)
.filter(|c| c.lines.is_some())
.zip(crate::govlog::LineRange::new(first, Some(last)).ok())
{
Some((c, cut)) => {
let cut = crate::govlog::DataCitation {
lines: Some(cut),
..c.clone()
};
format!("id=\"{cut}\"")
}
None => format!("lines=\"{first}-{last}\""),
};
format!(
"Read it whole in a fresh turn ({tokens}), or {range} now (about \
{fit} lines).",
tokens = crate::threads::about_tokens(text.len() as u64),
)
}
#[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 mut result = self.inner.call(call).await;
if let Some(once) = &self.shown_once
&& super::tool::raw_text(&result.content)
.contains(crate::links::FORM)
&& !once.legend("link form")
{
super::tool::map_text(&mut result.content, |t| {
t.replace(crate::links::FORM, crate::links::FORM_AGAIN)
});
}
if super::tool::is_refusal(&result)
&& !super::tool::raw_text(&result.content)
.ends_with(super::tool::REFUSED)
{
super::tool::append_sentence(
&mut result.content,
super::tool::REFUSED,
);
}
if let Some(note) = super::tool::read_as(&name, &input) {
super::tool::prepend_text(&mut result.content, ¬e);
}
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("~10000 tok;"), "{note}");
assert!(note.contains("~20000 tok 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");
}
struct Marks {
len: usize,
once: super::super::ShownOnce,
id: crate::ids::ContentId,
}
#[async_trait::async_trait]
impl Tool for Marks {
fn name(&self) -> &str {
"marks"
}
fn definitions(&self) -> Vec<MethodDef> {
Vec::new()
}
async fn call(&mut self, call: Use) -> tool::Result {
self.once.begin("your read of 7ad26ccd");
self.once.mark(self.id, self.len, self.len, None);
tool::Result::new(call.id, "x".repeat(self.len))
}
}
#[tokio::test]
async fn a_result_left_out_marks_nothing_shown() {
let once = super::super::ShownOnce::default();
let id = crate::ids::ContentId::from(uuid::Uuid::from_u128(7));
let gauge = ContextGauge::new(60_000, CONTEXT_BUFFER_TOKENS);
gauge.set(10_000);
let mut big = Gauged::new(
Marks {
len: 90_000,
once: once.clone(),
id,
},
gauge.clone(),
)
.with_shown_once(once.clone());
assert!(big.call(call("get_content")).await.is_error);
assert!(!once.contains(id));
let mut small = Gauged::new(
Marks {
len: 300,
once: once.clone(),
id,
},
gauge.clone(),
)
.with_shown_once(once.clone());
assert!(!small.call(call("get_content")).await.is_error);
assert_eq!(once.earlier(id).unwrap().bytes, 300);
}
#[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: ~20000 tok"),
"{}",
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 ~"));
}
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)
);
}
#[tokio::test]
async fn a_refused_data_file_read_offers_lines_that_fit() {
let text: String = (0..1_800)
.map(|_| format!("{}\n", "x".repeat(99)))
.collect();
let note = |input: serde_json::Value| {
let gauge = ContextGauge::new(131_072, CONTEXT_BUFFER_TOKENS);
gauge.set(gauge.window() - gauge.reserve() - 10_000);
let mut tool = Gauged::new(Fixed { text: text.clone() }, gauge);
async move {
tool.call(call_with("get_content", input))
.await
.content
.to_string()
}
};
let whole = note(serde_json::json!({
"id": "DATA-2026-0001", "attachment": "votes/raw.json"
}))
.await;
assert!(
whole.contains("lines=\"1-259\" now (about 259 lines"),
"{whole}"
);
assert!(
whole.contains(
"Read it whole in a fresh turn (~60k tok), or \
lines=\"1-259\" now (about 259 lines)."
),
"{whole}"
);
let cited = note(serde_json::json!({
"id": "DATA-2026-0001:89fe7c6:votes/raw.json:500-3000"
}))
.await;
assert!(
cited.contains(
"id=\"DATA-2026-0001:89fe7c6:votes/raw.json:500-758\""
),
"{cited}"
);
let pinned = note(serde_json::json!({
"id": "DATA-2026-0001:/votes/raw.json:500-3000"
}))
.await;
assert!(
pinned.contains("id=\"DATA-2026-0001:/votes/raw.json:500-758\""),
"{pinned}"
);
let listing = note(serde_json::json!({
"id": "DATA-2026-0001", "attachment": "votes/"
}))
.await;
assert!(!listing.contains("lines="), "{listing}");
assert!(listing.contains("a smaller comment_budget"), "{listing}");
}
#[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);
}
#[test]
fn a_refused_link_read_names_link_options() {
let by_link = Requested::of(
"fetch_agora_link",
&serde_json::json!({"link": "agora://gov/GOV-2026-0016"}),
);
assert_eq!(by_link.tool("fetch_agora_link"), "get_content");
let advice = by_link.advice("fetch_agora_link", 1_000, "x");
assert!(advice.ends_with(LINK_OPTIONS), "{advice}");
let by_id = Requested::of(
"get_content",
&serde_json::json!({"id": "GOV-2026-0016"}),
);
assert!(
!by_id
.advice("get_content", 1_000, "x")
.contains(LINK_OPTIONS)
);
}
}