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::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
19/// 提取 MessageContent 中的文本
20fn message_content_text(content: &MessageContent) -> &str {
21    match content {
22        MessageContent::Text(t) => t.as_str(),
23        _ => "[非文本内容]",
24    }
25}
26
27/// 组合 context 配置
28/// 作用:根据 session_id 和 load_type 加载历史消息,检查 token 阈值并触发压缩
29/// 关联:被 agent loop 调用
30/// 预期结果:返回 messages 数组(包含 system prompt),供 AI 调用使用
31pub async fn combine_context(
32    options: &CombineContextOptions,
33) -> Result<Vec<Message>, ContextError> {
34    // 参数有效性检查
35    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    // 根据 load_type 加载数据
47    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    // 处理压缩记录,找到最新的压缩内容和起始 entry_id
68    let (compacted_summary, start_entry_id) = process_compacted_entries(&session_data);
69
70    // 根据 start_entry_id 过滤 entries
71    let filtered_entries = filter_entries_from_start(&session_data.entries, &start_entry_id);
72
73    // 更新 session_data 的 entries 为过滤后的结果
74    let mut filtered_session = session_data;
75    filtered_session.entries = filtered_entries;
76
77    // 根据 provider 类型转换数据格式
78    let messages = transform_to_messages(
79        &filtered_session,
80        &options.provider,
81        compacted_summary.as_deref(),
82    );
83
84    // 计算 token 用量并检查是否需要压缩
85    let mut context_tokens: u32 = 0;
86
87    // 获取最后一条 assistant 消息的 token_consumption
88    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    // 最多检查 3 条 assistant 消息
96    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    // 如果没有找到 token_consumption,或者找到后还有新消息,需要估算
115    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    // 检查是否需要压缩
150    let should_compress = check_threshold(
151        context_tokens,
152        options.context_window,
153        options.threshold_percentage,
154    );
155
156    // 构建最终的 messages
157    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        // 实际执行压缩
172        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    // 在返回前添加 system prompt 到最前面
204    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
220/// 处理压缩记录,找到最新的压缩内容和起始 entry_id
221fn 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
245/// 递归查找最终的 trigger_entry_id
246fn 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(&current_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
278/// 从 start_entry_id 开始过滤 entries
279fn 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
296/// 将 session 数据转换为 AI 请求的 messages 格式
297fn 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
315/// 转换为 OpenAI 格式的 messages
316fn 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
374/// 转换为 Anthropic 格式的 messages
375fn 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}