1mod cache;
2mod replay;
3mod tokens;
4
5pub use cache::{ContextCache, ContextCacheEntry, FileFingerprint, conversation_cache_material};
6#[cfg(test)]
7pub(crate) use replay::{
8 ConversationReplay, REPLAY_TOOL_RESULT_OUTPUT_CHAR_LIMIT,
9 build_conversation_replay_with_limits, compact_historical_tool_output,
10};
11pub(crate) use replay::{
12 ConversationReplayLimits, REPLAY_JSONL_MAX_BYTES, REPLAY_JSONL_MAX_LINES,
13 build_conversation_replay, build_conversation_replay_from_events,
14};
15#[cfg(test)]
16pub(crate) use tokens::estimate_provider_request_input_tokens;
17pub use tokens::{
18 ContextBudget, ContextBudgetOverride, estimate_messages_tokens, estimate_text_tokens,
19 usage_input_tokens,
20};
21pub(crate) use tokens::{
22 ContextTokenCount, project_provider_conversation_items_tokens,
23 project_provider_request_input_tokens, project_text_tokens,
24};
25
26#[cfg(test)]
27pub(crate) use crate::providers::{ChatMessage, ProviderConversationItem, ProviderToolResult};
28use crate::sessions::{
29 Session, SessionDiagnosticOperation, SessionEvent, SessionEventKind, SessionReadDiagnostic,
30 report_session_diagnostic,
31};
32use serde_json::Value;
33#[cfg(test)]
34pub(crate) use serde_json::json;
35
36const RECENT_EVENTS_CONTEXT_MAX_EVENTS: usize = 10_000;
37const RECENT_EVENTS_CONTEXT_MAX_BYTES: usize = 2 * 1024 * 1024;
38const RECENT_EVENTS_CONTEXT_MAX_READ_BYTES: usize = RECENT_EVENTS_CONTEXT_MAX_BYTES;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct RecentEventsContext {
43 pub text: String,
44 pub diagnostics: Vec<SessionReadDiagnostic>,
45}
46
47pub fn recent_events_context(
48 session: &Session,
49 keep_recent_tokens: usize,
50) -> anyhow::Result<String> {
51 Ok(recent_events_context_report(session, keep_recent_tokens)?.text)
52}
53
54pub fn recent_events_context_report(
55 session: &Session,
56 keep_recent_tokens: usize,
57) -> anyhow::Result<RecentEventsContext> {
58 let mut selected = Vec::new();
59 let mut tokens = 0;
60 let recent = session
61 .read_recent_events_tolerant_tail(
62 RECENT_EVENTS_CONTEXT_MAX_EVENTS,
63 RECENT_EVENTS_CONTEXT_MAX_BYTES,
64 RECENT_EVENTS_CONTEXT_MAX_READ_BYTES,
65 )
66 .inspect_err(|error| {
67 report_session_diagnostic(
68 SessionDiagnosticOperation::RecentContext,
69 session.path(),
70 error,
71 );
72 })?;
73 for event in recent.events.into_iter().rev() {
74 if !is_provider_visible_session_event(&event) {
75 continue;
76 }
77 let line = event_line(&event);
78 let line_tokens = estimate_text_tokens(&line);
79 if !selected.is_empty() && tokens + line_tokens > keep_recent_tokens {
80 break;
81 }
82 tokens += line_tokens;
83 selected.push(line);
84 }
85 selected.reverse();
86 Ok(RecentEventsContext {
87 text: selected.join("\n"),
88 diagnostics: recent.diagnostics,
89 })
90}
91
92fn is_provider_visible_session_event(event: &SessionEvent) -> bool {
93 !is_local_only_session_event(event) && event.kind() != Some(SessionEventKind::ReasoningSummary)
94}
95
96pub(crate) fn is_local_only_session_event(event: &SessionEvent) -> bool {
97 event.kind().is_some_and(SessionEventKind::is_local_only)
98}
99
100pub(crate) fn event_line(event: &SessionEvent) -> String {
101 let compact_payload = if event.kind() == Some(SessionEventKind::Compaction) {
102 event
103 .payload
104 .get("summary")
105 .and_then(Value::as_str)
106 .unwrap_or("[invalid compaction summary]")
107 .to_string()
108 } else if let Some(text) = event.payload.get("text").and_then(Value::as_str) {
109 text.to_string()
110 } else if let Some(message) = event.payload.get("message").and_then(Value::as_str) {
111 message.to_string()
112 } else {
113 event.payload.to_string()
114 };
115 format!(
116 "{}: {}",
117 event.event_type,
118 compact_payload.replace('\n', "\\n")
119 )
120}
121
122pub(crate) fn canonical_json(value: &Value) -> String {
123 serde_json::to_string(value).unwrap_or_else(|_| "null".to_string())
124}
125
126#[cfg(test)]
127mod tests;