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