use serde::{Deserialize, Serialize};
use crate::frame::{ContextFrame, FrameKind, Representation};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextQuery {
pub goal: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub query_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embedding: Option<Vec<f32>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub kinds: Vec<FrameKind>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub anchors: Vec<String>,
pub max_frames: u32,
pub max_tokens: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub as_of: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub representation_preferences: Vec<Representation>,
}
impl ContextQuery {
pub fn preferred_representations(&self) -> Vec<Representation> {
if self.representation_preferences.is_empty() {
vec![Representation::Full]
} else {
self.representation_preferences.clone()
}
}
pub fn select_representation(&self, supported: &[Representation]) -> Option<Representation> {
self.preferred_representations()
.into_iter()
.find(|wanted| supported.contains(wanted))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextQueryResult {
pub frames: Vec<ContextFrame>,
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dropped_estimate: Option<u32>,
}
impl ContextQueryResult {
pub fn total_token_cost(&self) -> u64 {
self.frames.iter().map(|f| f.token_cost as u64).sum()
}
pub fn respects_budget(&self, max_tokens: u32) -> bool {
self.total_token_cost() <= max_tokens as u64
}
pub fn respects_frame_limit(&self, max_frames: u32) -> bool {
self.frames.len() as u64 <= max_frames as u64
}
pub fn frames_with_dishonest_cost(&self) -> Vec<&str> {
self.frames
.iter()
.filter(|f| !f.declares_honest_token_cost())
.map(|f| f.id.as_str())
.collect()
}
pub fn canonical_token_cost(&self) -> u64 {
self.frames
.iter()
.map(|f| f.expected_inline_token_cost() as u64)
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frame::ContextFrame;
fn frame_with_cost(id: &str, cost: u32) -> ContextFrame {
ContextFrame::full(id, FrameKind::Snippet, id, String::new(), 0.5, cost)
}
#[test]
fn context_query_roundtrips() {
let query = ContextQuery {
goal: "fix the failing test".into(),
query_text: Some("failing test".into()),
embedding: None,
kinds: vec![FrameKind::Symbol, FrameKind::Doc],
anchors: vec!["file:///repo/src/lib.rs".into()],
max_frames: 20,
max_tokens: 4000,
as_of: None,
representation_preferences: vec![],
};
let json = serde_json::to_string(&query).unwrap();
let back: ContextQuery = serde_json::from_str(&json).unwrap();
assert_eq!(back, query);
}
#[test]
fn representation_preferences_default_to_full_and_select_first_supported() {
let mut query = ContextQuery {
goal: "g".into(),
query_text: None,
embedding: None,
kinds: vec![],
anchors: vec![],
max_frames: 1,
max_tokens: 10,
as_of: None,
representation_preferences: vec![],
};
assert_eq!(
query.preferred_representations(),
vec![Representation::Full]
);
assert!(
!serde_json::to_string(&query)
.unwrap()
.contains("representation_preferences")
);
assert_eq!(
query.select_representation(&[Representation::Full]),
Some(Representation::Full)
);
query.representation_preferences = vec![Representation::Reference, Representation::Full];
assert_eq!(
query.select_representation(&[Representation::Full]),
Some(Representation::Full),
);
assert_eq!(
query.select_representation(&[Representation::Reference, Representation::Full]),
Some(Representation::Reference),
);
assert_eq!(
query.select_representation(&[Representation::Compact]),
None
);
}
#[test]
fn respects_budget_true_when_under_or_at_limit() {
let result = ContextQueryResult {
frames: vec![frame_with_cost("a", 100), frame_with_cost("b", 200)],
truncated: false,
dropped_estimate: None,
};
assert_eq!(result.total_token_cost(), 300);
assert!(result.respects_budget(300));
assert!(result.respects_budget(500));
}
#[test]
fn respects_budget_false_when_provider_lies_about_cost() {
let result = ContextQueryResult {
frames: vec![frame_with_cost("a", 400)],
truncated: false,
dropped_estimate: None,
};
assert!(!result.respects_budget(300));
}
fn honest_frame(id: &str, content: &str) -> ContextFrame {
let mut frame = frame_with_cost(id, 0);
frame.content = Some(content.to_string());
frame.token_cost = frame.expected_inline_token_cost();
frame
}
#[test]
fn frame_limit_catches_the_provider_that_floods_with_cheap_frames() {
let flood = ContextQueryResult {
frames: (0..50)
.map(|i| honest_frame(&format!("f{i}"), "x"))
.collect(),
truncated: false,
dropped_estimate: None,
};
assert!(flood.respects_budget(10_000), "the token budget is fine");
assert!(!flood.respects_frame_limit(8), "but the frame cap is not");
assert!(flood.respects_frame_limit(50), "boundary is inclusive");
}
#[test]
fn an_honest_result_reports_no_dishonest_frames() {
let result = ContextQueryResult {
frames: vec![honest_frame("a", "abcd"), honest_frame("b", "abcdefgh")],
truncated: false,
dropped_estimate: None,
};
assert!(result.frames_with_dishonest_cost().is_empty());
assert_eq!(result.total_token_cost(), result.canonical_token_cost());
}
#[test]
fn dishonest_frames_are_named_and_the_true_cost_is_recoverable() {
let mut liar = honest_frame("liar", &"x".repeat(4_000));
liar.token_cost = 1; let result = ContextQueryResult {
frames: vec![honest_frame("honest", "abcd"), liar],
truncated: false,
dropped_estimate: None,
};
assert_eq!(result.frames_with_dishonest_cost(), vec!["liar"]);
assert_eq!(result.total_token_cost(), 2);
assert_eq!(result.canonical_token_cost(), 1_001);
assert!(result.respects_budget(100));
}
}