mod cache;
mod replay;
mod tokens;
pub use cache::{ContextCache, ContextCacheEntry, FileFingerprint, conversation_cache_material};
#[cfg(test)]
pub(crate) use replay::{
ConversationReplay, REPLAY_TOOL_RESULT_OUTPUT_CHAR_LIMIT,
build_conversation_replay_with_limits, compact_historical_tool_output,
};
pub(crate) use replay::{
ConversationReplayLimits, REPLAY_JSONL_MAX_BYTES, REPLAY_JSONL_MAX_LINES,
build_conversation_replay, build_conversation_replay_from_events,
};
#[cfg(test)]
pub(crate) use tokens::estimate_provider_request_input_tokens;
pub use tokens::{
ContextBudget, ContextBudgetOverride, estimate_messages_tokens, estimate_text_tokens,
usage_input_tokens,
};
pub(crate) use tokens::{
ContextTokenCount, project_provider_conversation_items_tokens,
project_provider_request_input_tokens, project_text_tokens,
};
#[cfg(test)]
pub(crate) use crate::providers::{ChatMessage, ProviderConversationItem, ProviderToolResult};
use crate::sessions::{
Session, SessionDiagnosticOperation, SessionEvent, SessionEventKind, SessionReadDiagnostic,
report_session_diagnostic,
};
use serde_json::Value;
#[cfg(test)]
pub(crate) use serde_json::json;
const RECENT_EVENTS_CONTEXT_MAX_EVENTS: usize = 10_000;
const RECENT_EVENTS_CONTEXT_MAX_BYTES: usize = 2 * 1024 * 1024;
const RECENT_EVENTS_CONTEXT_MAX_READ_BYTES: usize = RECENT_EVENTS_CONTEXT_MAX_BYTES;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecentEventsContext {
pub text: String,
pub diagnostics: Vec<SessionReadDiagnostic>,
}
pub fn recent_events_context(
session: &Session,
keep_recent_tokens: usize,
) -> anyhow::Result<String> {
Ok(recent_events_context_report(session, keep_recent_tokens)?.text)
}
pub fn recent_events_context_report(
session: &Session,
keep_recent_tokens: usize,
) -> anyhow::Result<RecentEventsContext> {
let mut selected = Vec::new();
let mut tokens = 0;
let recent = session
.read_recent_events_tolerant_tail(
RECENT_EVENTS_CONTEXT_MAX_EVENTS,
RECENT_EVENTS_CONTEXT_MAX_BYTES,
RECENT_EVENTS_CONTEXT_MAX_READ_BYTES,
)
.inspect_err(|error| {
report_session_diagnostic(
SessionDiagnosticOperation::RecentContext,
session.path(),
error,
);
})?;
for event in recent.events.into_iter().rev() {
if !is_provider_visible_session_event(&event) {
continue;
}
let line = event_line(&event);
let line_tokens = estimate_text_tokens(&line);
if !selected.is_empty() && tokens + line_tokens > keep_recent_tokens {
break;
}
tokens += line_tokens;
selected.push(line);
}
selected.reverse();
Ok(RecentEventsContext {
text: selected.join("\n"),
diagnostics: recent.diagnostics,
})
}
fn is_provider_visible_session_event(event: &SessionEvent) -> bool {
!is_local_only_session_event(event) && event.kind() != Some(SessionEventKind::ReasoningSummary)
}
pub(crate) fn is_local_only_session_event(event: &SessionEvent) -> bool {
event.kind().is_some_and(SessionEventKind::is_local_only)
}
pub(crate) fn event_line(event: &SessionEvent) -> String {
let compact_payload = if event.kind() == Some(SessionEventKind::Compaction) {
event
.payload
.get("summary")
.and_then(Value::as_str)
.unwrap_or("[invalid compaction summary]")
.to_string()
} else if let Some(text) = event.payload.get("text").and_then(Value::as_str) {
text.to_string()
} else if let Some(message) = event.payload.get("message").and_then(Value::as_str) {
message.to_string()
} else {
event.payload.to_string()
};
format!(
"{}: {}",
event.event_type,
compact_payload.replace('\n', "\\n")
)
}
pub(crate) fn canonical_json(value: &Value) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "null".to_string())
}
#[cfg(test)]
mod tests;