Skip to main content

otherone_context/
lib.rs

1// 作用:otherone-context 模块 — 上下文管理(会话加载 + 压缩触发)
2// 关联:被 otherone-agent 的 invoke_agent 调用
3// 预期结果:提供 combine_context 核心方法
4
5pub 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
18/// 提取 MessageContent 中的文本
19fn message_content_text(content: &MessageContent) -> &str {
20    match content {
21        MessageContent::Text(t) => t.as_str(),
22        _ => "[非文本内容]",
23    }
24}
25
26/// 组合 context 配置
27/// 作用:根据 session_id 和 load_type 加载历史消息,检查 token 阈值并触发压缩
28/// 关联:被 agent loop 调用
29/// 预期结果:返回 messages 数组(包含 system prompt),供 AI 调用使用
30pub async fn combine_context(
31    options: &CombineContextOptions,
32) -> Result<Vec<Message>, ContextError> {
33    // 参数有效性检查
34    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    // 根据 load_type 加载数据
46    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    // 处理压缩记录,找到最新的压缩内容和起始 entry_id
73    let (compacted_summary, start_entry_id) = process_compacted_entries(&session_data);
74
75    // 根据 start_entry_id 过滤 entries
76    let filtered_entries = filter_entries_from_start(&session_data.entries, &start_entry_id);
77
78    // 更新 session_data 的 entries 为过滤后的结果
79    let mut filtered_session = session_data;
80    filtered_session.entries = filtered_entries;
81
82    // 根据 provider 类型转换数据格式
83    let messages = transform_to_messages(
84        &filtered_session,
85        &options.provider,
86        compacted_summary.as_deref(),
87    );
88
89    // 计算 token 用量并检查是否需要压缩
90    let mut context_tokens: u32 = 0;
91
92    // 获取最后一条 assistant 消息的 token_consumption
93    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    // 最多检查 3 条 assistant 消息
101    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    // 如果没有找到 token_consumption,或者找到后还有新消息,需要估算
120    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    // 检查是否需要压缩
155    let should_compress = check_threshold(
156        context_tokens,
157        options.context_window,
158        options.threshold_percentage,
159    );
160
161    // 构建最终的 messages
162    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        // 实际执行压缩
177        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    // 在返回前添加 system prompt 到最前面
210    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
226/// 处理压缩记录,找到最新的压缩内容和起始 entry_id
227fn 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
251/// 递归查找最终的 trigger_entry_id
252fn 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(&current_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
284/// 从 start_entry_id 开始过滤 entries
285fn 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
302/// 将 session 数据转换为 AI 请求的 messages 格式
303fn 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
321/// 转换为 OpenAI 格式的 messages
322fn 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
380/// 转换为 Anthropic 格式的 messages
381fn 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}