1pub mod compact;
6pub mod error;
7pub mod types;
8
9use tracing::warn;
10
11use crate::compact::check_threshold::check_threshold;
12use crate::compact::compact_messages;
13use crate::compact::estimate_tokens::estimate_tokens;
14use crate::error::ContextError;
15use crate::types::{CombineContextOptions, ContextLoadType};
16use otherone_ai::types::{Message, MessageContent};
17use otherone_storage::types::SessionData;
18
19fn message_content_text(content: &MessageContent) -> &str {
21 match content {
22 MessageContent::Text(t) => t.as_str(),
23 _ => "[非文本内容]",
24 }
25}
26
27pub async fn combine_context(
32 options: &CombineContextOptions,
33) -> Result<Vec<Message>, ContextError> {
34 if options.session_id.is_empty() {
36 return Err(ContextError::ConfigError(
37 "session_id is required".to_string(),
38 ));
39 }
40 if options.context_window == 0 {
41 return Err(ContextError::ConfigError(
42 "context_window is required".to_string(),
43 ));
44 }
45
46 let session_data = match options.load_type {
48 ContextLoadType::Database => {
49 let db_config = options.database_config.as_ref().ok_or_else(|| {
50 ContextError::ConfigError(
51 "database_config is required when load_type is database".to_string(),
52 )
53 })?;
54 otherone_storage::database::reader::read_session_data_from_database(
55 &options.session_id,
56 db_config,
57 )
58 .await
59 .map_err(|e| ContextError::StorageError(e.to_string()))?
60 }
61 ContextLoadType::LocalFile => {
62 otherone_storage::localfile::reader::read_session_data(&options.session_id)
63 .map_err(|e| ContextError::StorageError(e.to_string()))?
64 }
65 };
66
67 let (compacted_summary, start_entry_id) = process_compacted_entries(&session_data);
69
70 let filtered_entries = filter_entries_from_start(&session_data.entries, &start_entry_id);
72
73 let mut filtered_session = session_data;
75 filtered_session.entries = filtered_entries;
76
77 let messages = transform_to_messages(
79 &filtered_session,
80 &options.provider,
81 compacted_summary.as_deref(),
82 );
83
84 let mut context_tokens: u32 = 0;
86
87 let assistant_entries: Vec<&otherone_storage::types::Entry> = filtered_session
89 .entries
90 .iter()
91 .rev()
92 .filter(|e| e.role == "assistant")
93 .collect();
94
95 let max_attempts = std::cmp::min(3, assistant_entries.len());
97 let mut found_token_index: Option<usize> = None;
98
99 for i in 0..max_attempts {
100 let entry = assistant_entries[i];
101 if let Some(tc) = entry.token_consumption {
102 context_tokens = tc;
103 if let Some(pos) = filtered_session
104 .entries
105 .iter()
106 .position(|e| e.entry_id == entry.entry_id)
107 {
108 found_token_index = Some(pos);
109 }
110 break;
111 }
112 }
113
114 if context_tokens == 0
116 || found_token_index.map_or(true, |idx| idx < filtered_session.entries.len() - 1)
117 {
118 let estimated = estimate_tokens(&messages);
119
120 if context_tokens == 0 {
121 let mut extra_tokens: u32 = 0;
122 if let Some(ref sp) = options.system_prompt {
123 let sys_msg = Message {
124 role: "system".to_string(),
125 content: MessageContent::Text(sp.clone()),
126 name: None,
127 tool_calls: None,
128 tool_call_id: None,
129 };
130 extra_tokens += estimate_tokens(&[sys_msg]);
131 }
132 if let Some(ref tools) = options.tools {
133 let tools_json = serde_json::to_string(tools).unwrap_or_default();
134 let tools_msg = Message {
135 role: "system".to_string(),
136 content: MessageContent::Text(tools_json),
137 name: None,
138 tool_calls: None,
139 tool_call_id: None,
140 };
141 extra_tokens += estimate_tokens(&[tools_msg]);
142 }
143 context_tokens = estimated + extra_tokens;
144 } else {
145 context_tokens += estimated;
146 }
147 }
148
149 let should_compress = check_threshold(
151 context_tokens,
152 options.context_window,
153 options.threshold_percentage,
154 );
155
156 let mut final_messages = messages;
158
159 if should_compress {
160 let has_compacted = final_messages.first().map_or(false, |m| {
161 m.role == "user"
162 && (message_content_text(&m.content).contains("[压缩")
163 || message_content_text(&m.content).contains("[Compressed"))
164 });
165
166 warn!(
167 "Context threshold exceeded ({} tokens / {} window). Triggering compaction.",
168 context_tokens, options.context_window
169 );
170
171 let storage_type = match options.load_type {
173 ContextLoadType::Database => otherone_storage::types::StorageType::Database,
174 ContextLoadType::LocalFile => otherone_storage::types::StorageType::LocalFile,
175 };
176
177 match compact_messages(
178 &final_messages,
179 context_tokens,
180 options.context_window,
181 None,
182 options.ai.as_ref(),
183 has_compacted,
184 Some(&options.session_id),
185 Some(&storage_type),
186 options.database_config.as_ref(),
187 Some(&filtered_session.entries),
188 )
189 .await
190 {
191 Ok(compacted) => {
192 final_messages = compacted;
193 }
194 Err(e) => {
195 warn!(
196 "Compaction failed: {}. Continuing with uncompacted messages.",
197 e
198 );
199 }
200 }
201 }
202
203 if let Some(ref system_prompt) = options.system_prompt {
205 final_messages.insert(
206 0,
207 Message {
208 role: "system".to_string(),
209 content: MessageContent::Text(system_prompt.clone()),
210 name: None,
211 tool_calls: None,
212 tool_call_id: None,
213 },
214 );
215 }
216
217 Ok(final_messages)
218}
219
220fn process_compacted_entries(session_data: &SessionData) -> (Option<String>, Option<String>) {
222 if session_data.compacted_entries.is_empty() {
223 return (None, None);
224 }
225
226 let mut sorted: Vec<&otherone_storage::types::CompactedEntry> =
227 session_data.compacted_entries.iter().collect();
228 sorted.sort_by(|a, b| b.create_at.cmp(&a.create_at));
229
230 let latest = sorted[0];
231
232 if latest.summary.is_empty() {
233 return (None, None);
234 }
235
236 let final_entry_id = find_final_trigger_entry_id(
237 &latest.trigger_entry_id,
238 &session_data.compacted_entries,
239 &session_data.entries,
240 );
241
242 (Some(latest.summary.clone()), final_entry_id)
243}
244
245fn find_final_trigger_entry_id(
247 trigger_id: &str,
248 compacted_entries: &[otherone_storage::types::CompactedEntry],
249 entries: &[otherone_storage::types::Entry],
250) -> Option<String> {
251 let max_depth = 10;
252 let mut current_id = trigger_id.to_string();
253 let mut visited_ids = std::collections::HashSet::new();
254
255 for _ in 0..max_depth {
256 if visited_ids.contains(¤t_id) {
257 warn!("检测到循环引用: {}", current_id);
258 return None;
259 }
260 visited_ids.insert(current_id.clone());
261
262 if entries.iter().any(|e| e.entry_id == current_id) {
263 return Some(current_id);
264 }
265
266 if let Some(compacted) = compacted_entries.iter().find(|c| c.entry_id == current_id) {
267 current_id = compacted.trigger_entry_id.clone();
268 } else {
269 warn!("trigger_entry_id 不存在: {}", current_id);
270 return None;
271 }
272 }
273
274 warn!("超过最大递归深度,最后的 ID: {}", current_id);
275 None
276}
277
278fn filter_entries_from_start(
280 entries: &[otherone_storage::types::Entry],
281 start_entry_id: &Option<String>,
282) -> Vec<otherone_storage::types::Entry> {
283 let start_id = match start_entry_id {
284 Some(id) => id,
285 None => return entries.to_vec(),
286 };
287
288 if let Some(start_index) = entries.iter().position(|e| e.entry_id == *start_id) {
289 entries[start_index..].to_vec()
290 } else {
291 warn!("start_entry_id 不存在于 entries 中: {}", start_id);
292 entries.to_vec()
293 }
294}
295
296fn transform_to_messages(
298 session_data: &SessionData,
299 provider: &otherone_ai::types::ProviderType,
300 compacted_summary: Option<&str>,
301) -> Vec<Message> {
302 match provider {
303 otherone_ai::types::ProviderType::OpenAI
304 | otherone_ai::types::ProviderType::OpenRouter
305 | otherone_ai::types::ProviderType::Local
306 | otherone_ai::types::ProviderType::Fetch => {
307 transform_to_openai_format(session_data, compacted_summary)
308 }
309 otherone_ai::types::ProviderType::Anthropic => {
310 transform_to_anthropic_format(session_data, compacted_summary)
311 }
312 }
313}
314
315fn transform_to_openai_format(
317 session_data: &SessionData,
318 compacted_summary: Option<&str>,
319) -> Vec<Message> {
320 let mut messages: Vec<Message> = Vec::new();
321
322 if let Some(summary) = compacted_summary {
323 messages.push(Message {
324 role: "user".to_string(),
325 content: MessageContent::Text(summary.to_string()),
326 name: None,
327 tool_calls: None,
328 tool_call_id: None,
329 });
330 }
331
332 if session_data.entries.is_empty() {
333 return messages;
334 }
335
336 for entry in &session_data.entries {
337 let mut message = Message {
338 role: entry.role.clone(),
339 content: MessageContent::Text(entry.content.clone()),
340 name: None,
341 tool_calls: None,
342 tool_call_id: None,
343 };
344
345 if entry.role == "assistant" {
346 if let Some(ref tools) = entry.tools {
347 if let Some(tool_calls) = tools.get("tool_calls") {
348 if let Ok(parsed_tool_calls) = serde_json::from_value(tool_calls.clone()) {
349 message.tool_calls = Some(parsed_tool_calls);
350 }
351 }
352 }
353 }
354
355 if entry.role == "tool" {
356 if let Some(ref tools) = entry.tools {
357 message.tool_call_id = tools
358 .get("tool_call_id")
359 .and_then(|v| v.as_str())
360 .map(|s| s.to_string());
361 message.name = tools
362 .get("function_name")
363 .and_then(|v| v.as_str())
364 .map(|s| s.to_string());
365 }
366 }
367
368 messages.push(message);
369 }
370
371 messages
372}
373
374fn transform_to_anthropic_format(
376 session_data: &SessionData,
377 compacted_summary: Option<&str>,
378) -> Vec<Message> {
379 transform_to_openai_format(session_data, compacted_summary)
380}