Skip to main content

magi_code/context/
mod.rs

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;
38// Tail read cap matches retained-byte budget: one bounded read can fill recent-context window.
39const 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;