use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::session::ResponseFormat;
use crate::telemetry::MAX_PAYLOAD_TOKENS;
pub fn default_budget(tool: &str, items: usize) -> usize {
match tool {
"get_file_outline" => 2_000,
"find_references" | "search_symbols" => 1_500,
"get_enclosing_scope" => 600.max(100 * items.min(25)),
"inspect_symbol" => MAX_PAYLOAD_TOKENS.max(500 * items.min(5)),
_ => MAX_PAYLOAD_TOKENS,
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct Continuation {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_offset: Option<usize>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub see_also: Vec<String>,
#[serde(skip)]
pub index_state: Option<String>,
}
pub(crate) fn is_zero(n: &usize) -> bool {
*n == 0
}
pub trait Paged: Serialize + Clone {
const TOOL: &'static str;
fn item_count(&self) -> usize;
fn page_offset(&self) -> usize;
fn keep_first(&mut self, kept: usize);
fn continuation_mut(&mut self) -> &mut Continuation;
fn render_text(&self) -> String;
fn render(&self, format: ResponseFormat) -> String {
match format {
ResponseFormat::Json => serde_json::to_string(self).unwrap_or_default(),
ResponseFormat::Text => self.render_text(),
}
}
}
pub fn fit_list_to_budget<P: Paged>(
out: &mut P,
budget: Option<usize>,
no_truncate: bool,
format: ResponseFormat,
count_tokens: impl Fn(&str) -> usize,
) {
let budget = budget.unwrap_or(MAX_PAYLOAD_TOKENS);
let size = |r: &P| count_tokens(&r.render(format));
if no_truncate || budget == 0 || size(out) <= budget {
return;
}
let full = out.clone();
let len = full.item_count();
if len < 2 {
return;
}
let offset = full.page_offset();
let base = full.clone().continuation_mut().clone();
let apply = |out: &mut P, kept: usize| {
*out = full.clone();
out.keep_first(kept);
let next = offset + kept;
let hint = format!("{}(same arguments, offset: {next}) for the rest", P::TOOL);
let c = out.continuation_mut();
*c = base.clone();
c.complete = Some(false);
c.next_offset = Some(next);
c.see_also.push(hint);
};
let (mut lo, mut hi) = (1, len - 1);
while lo < hi {
let mid = (lo + hi).div_ceil(2);
apply(out, mid);
if size(out) <= budget {
lo = mid;
} else {
hi = mid - 1;
}
}
apply(out, lo);
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Serialize)]
struct Page {
items: Vec<String>,
offset: usize,
returned: usize,
has_more: bool,
#[serde(flatten)]
next: Continuation,
}
impl Paged for Page {
const TOOL: &'static str = "demo";
fn item_count(&self) -> usize {
self.items.len()
}
fn page_offset(&self) -> usize {
self.offset
}
fn keep_first(&mut self, kept: usize) {
self.items.truncate(kept);
self.returned = kept;
self.has_more = true;
}
fn continuation_mut(&mut self) -> &mut Continuation {
&mut self.next
}
fn render_text(&self) -> String {
self.items.join("\n")
}
}
fn chars(s: &str) -> usize {
s.len()
}
fn page(n: usize) -> Page {
Page {
items: (0..n).map(|i| format!("item{i:03}")).collect(),
offset: 10,
returned: n,
has_more: false,
next: Continuation::default(),
}
}
#[test]
fn a_page_that_fits_is_left_alone() {
let mut p = page(3);
fit_list_to_budget(&mut p, Some(10_000), false, ResponseFormat::Json, chars);
assert_eq!(p.items.len(), 3);
assert!(p.next.complete.is_none() && p.next.see_also.is_empty());
}
#[test]
fn an_oversized_page_keeps_the_largest_prefix_and_names_the_next_call() {
let mut p = page(200);
fit_list_to_budget(&mut p, Some(600), false, ResponseFormat::Json, chars);
let json = serde_json::to_string(&p).unwrap();
assert!(json.len() <= 600, "{}", json.len());
let kept = p.items.len();
assert!((1..200).contains(&kept));
assert_eq!(p.items[kept - 1], format!("item{:03}", kept - 1));
assert_eq!(p.returned, kept);
assert!(p.has_more);
assert_eq!(p.next.complete, Some(false));
assert_eq!(p.next.next_offset, Some(10 + kept));
assert!(p.next.see_also[0].contains(&format!("offset: {}", 10 + kept)));
}
#[test]
fn no_truncate_and_a_zero_budget_disable_fitting() {
let mut p = page(200);
fit_list_to_budget(&mut p, Some(100), true, ResponseFormat::Json, chars);
fit_list_to_budget(&mut p, Some(0), false, ResponseFormat::Json, chars);
assert_eq!(p.items.len(), 200);
}
}