Skip to main content

j_agent/context/
message_compress.rs

1//! 消息压缩模块:压缩来自其他 agent 的 tool call 消息
2//!
3//! 在 teammate/subagent 调用 LLM 前,对消息列表进行压缩:
4//! - 保留最近 N 条完整的 tool call 消息
5//! - 较早的消息压缩为摘要,减少上下文占用
6//!
7//! # 上下文注入机制(Feature,非 Bug)
8//!
9//! SubAgent/Teammate 通过各自推送逻辑写入双通道:
10//! - `display_messages`:干净文本 + sender_name 字段 → UI 渲染
11//! - `context_messages`:XML 包裹(如 `<AgentName>text</AgentName>`) → LLM context
12//!
13//! 数据流:
14//! - `display_messages` → UI 渲染数据源
15//! - `context_messages` → `build_api_messages`(LLM context 数据源)
16//!
17//! 本模块的压缩功能用于减少这些消息对上下文的占用,而非完全过滤它们。
18//! 参见 `agent/tool_processor.rs` 中 `push_both` 函数的文档注释。
19//!
20//! # 压缩策略
21//!
22//! 广播消息格式:`<Type: AgentName> [调用工具 ToolName]`
23//! - 按 agent 来源分组
24//! - 保留最近 threshold 条完整消息
25//! - 较早的合并为摘要:`<Type: AgentName> [早期工具调用摘要: ToolA×5, ToolB×3, 共 8 次]`
26
27use crate::storage::{ChatMessage, MessageRole};
28use std::collections::HashMap;
29
30/// 默认压缩阈值:保留最近 5 条完整消息
31pub const DEFAULT_OTHER_AGENT_TOOLCALL_THRESHOLD: usize = 5;
32
33/// 从消息内容中提取 agent 来源
34///
35/// 广播消息格式:`<Type: AgentName> message` 或 `<AgentName> message`(旧格式兼容)
36/// 返回 (agent_name, remainder) 或 None(非广播消息)
37fn extract_agent_source(content: &str) -> Option<(String, &str)> {
38    let trimmed = content.trim_start();
39    if !trimmed.starts_with('<') {
40        return None;
41    }
42    let end_bracket = trimmed.find('>')?;
43    let agent_name = trimmed[1..end_bracket].to_string();
44    let remainder = &trimmed[end_bracket + 1..];
45    Some((agent_name, remainder))
46}
47
48/// 判断是否为 tool call 广播消息
49///
50/// 格式:`<Type: AgentName> [调用工具 ToolName]` 或 `<AgentName> [调用工具 ToolName]`
51fn is_tool_call_broadcast(content: &str) -> Option<(String, String)> {
52    let (agent_name, remainder) = extract_agent_source(content)?;
53    let trimmed = remainder.trim_start();
54    if !trimmed.starts_with("[调用工具 ") {
55        return None;
56    }
57    let end_bracket = trimmed.find(']')?;
58    let tool_name = trimmed["[调用工具 ".len()..end_bracket].to_string();
59    Some((agent_name, tool_name))
60}
61
62/// 压缩来自其他 agent 的 tool call 消息
63///
64/// # 参数
65///
66/// - `messages`: 原始消息列表
67/// - `self_agent_name`: 当前 agent 名(Main/SubAgent 名或 Teammate 名)
68/// - `threshold`: 保留最近多少条完整消息
69///
70/// # 返回
71///
72/// 压缩后的消息列表:
73/// - 自己的消息保留完整
74/// - 其他 agent 的 tool call:最近 threshold 条保留,较早的压缩为摘要
75pub fn compress_other_agent_toolcalls(
76    messages: &[ChatMessage],
77    self_agent_name: &str,
78    threshold: usize,
79) -> Vec<ChatMessage> {
80    if messages.is_empty() || threshold == 0 {
81        return messages.to_vec();
82    }
83
84    // 1. 收集所有来自其他 agent 的 tool call 消息及其索引
85    let other_agent_tool_calls: Vec<(usize, String, String)> = messages
86        .iter()
87        .enumerate()
88        .filter_map(|(idx, msg)| {
89            // 处理 User role(通过 pending_user_messages 接收的广播)
90            // 和 Assistant role(通过 context_messages 同步的 teammate/subagent 活动消息)
91            let content = &msg.content;
92            let (agent_name, tool_name) = is_tool_call_broadcast(content)?;
93            // 排除自己发出的 tool call 广播
94            if agent_name == self_agent_name {
95                return None;
96            }
97            Some((idx, agent_name, tool_name))
98        })
99        .collect();
100
101    // 无其他 agent 的 tool call,直接返回原列表
102    if other_agent_tool_calls.is_empty() {
103        return messages.to_vec();
104    }
105
106    // 2. 按 agent 来源分组,记录每个 agent 的消息索引和工具名
107    let agent_groups: HashMap<String, Vec<(usize, String)>> =
108        other_agent_tool_calls
109            .iter()
110            .fold(HashMap::new(), |mut acc, (idx, agent, tool)| {
111                acc.entry(agent.clone())
112                    .or_default()
113                    .push((*idx, tool.clone()));
114                acc
115            });
116
117    // 3. 对于每个 agent,决定哪些索引需要压缩
118    let mut indices_to_compress: Vec<usize> = Vec::new(); // 大小依赖运行时计算
119    let mut summary_by_first_idx: HashMap<usize, (String, HashMap<String, usize>)> = HashMap::new();
120
121    for (agent_name, calls) in agent_groups {
122        let total = calls.len();
123        if total <= threshold {
124            // 不需要压缩
125            continue;
126        }
127
128        // 保留最近 threshold 条(索引大的)
129        let recent_start = total - threshold;
130        let (to_compress, _to_keep) = calls.split_at(recent_start);
131
132        // 记录需要压缩的索引
133        for (idx, _) in to_compress {
134            indices_to_compress.push(*idx);
135        }
136
137        // 统计压缩部分的工具调用次数
138        let tool_counts: HashMap<String, usize> =
139            to_compress
140                .iter()
141                .fold(HashMap::new(), |mut acc, (_, tool)| {
142                    *acc.entry(tool.clone()).or_default() += 1;
143                    acc
144                });
145
146        // 记录摘要信息,放在该 agent 最早出现的位置(to_compress 的第一个索引)
147        if let Some((first_idx, _)) = to_compress.first() {
148            summary_by_first_idx.insert(*first_idx, (agent_name.clone(), tool_counts));
149        }
150    }
151
152    // 4. 构建压缩后的消息列表
153    let mut result: Vec<ChatMessage> = Vec::with_capacity(messages.len());
154    for (idx, msg) in messages.iter().enumerate() {
155        if let Some((agent_name, tool_counts)) = summary_by_first_idx.get(&idx) {
156            // 在此位置插入压缩摘要
157            let total_calls: usize = tool_counts.values().sum();
158            let tools_summary: String = tool_counts
159                .iter()
160                .map(|(tool, count)| format!("{}×{}", tool, count))
161                .collect::<Vec<_>>()
162                .join(", ");
163            let summary_content = format!(
164                "<{}> [早期工具调用摘要: {}, 共 {} 次]</{}>",
165                agent_name, tools_summary, total_calls, agent_name
166            );
167            result.push(ChatMessage::text(MessageRole::User, summary_content));
168        }
169
170        if indices_to_compress.contains(&idx) {
171            // 跳过被压缩的消息
172            continue;
173        }
174
175        // 保留原消息(未被压缩的)
176        result.push(msg.clone());
177    }
178
179    result
180}
181
182#[cfg(test)]
183mod tests;