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