Compare commits

..

No commits in common. "2d5b6168cc3f131a005a65407d7bb71063bec134" and "8f82009c320014f7b10d1bfafa046752f976db66" have entirely different histories.

6 changed files with 56 additions and 117 deletions

View File

@ -11,19 +11,31 @@ use crate::agent::{AgentError, AgentRuntimeConfig};
const TOKEN_ESTIMATE_CHARS_PER_TOKEN: usize = 4;
const TOKEN_ESTIMATE_SAFETY_MULTIPLIER: f64 = 1.2;
/// Token estimation using JSON serialization (matches actual request size)
/// Token estimation using ~4 chars/token heuristic with 1.2x safety margin.
pub fn estimate_tokens(messages: &[ChatMessage]) -> usize {
// Serialize to JSON to match actual request format sent to LLM
let serialized_len = serde_json::to_string(messages)
.map(|s| s.len())
.unwrap_or_else(|_| {
// Fallback: use content length if serialization fails
messages.iter().map(|m| m.content.len()).sum()
});
let raw: usize = messages
.iter()
.map(|message| {
message
.content
.len()
.div_ceil(TOKEN_ESTIMATE_CHARS_PER_TOKEN)
+ estimate_image_tokens(&message.media_refs)
+ 4
})
.sum();
(raw as f64 * TOKEN_ESTIMATE_SAFETY_MULTIPLIER) as usize
}
// Apply safety margin for token estimation
((serialized_len / TOKEN_ESTIMATE_CHARS_PER_TOKEN) as f64
* TOKEN_ESTIMATE_SAFETY_MULTIPLIER) as usize
fn estimate_image_tokens(media_refs: &[String]) -> usize {
media_refs
.iter()
.filter_map(|path| std::fs::metadata(path).ok())
.map(|metadata| {
let base64_chars = metadata.len().saturating_mul(4).div_ceil(3) as usize;
base64_chars.div_ceil(TOKEN_ESTIMATE_CHARS_PER_TOKEN)
})
.sum()
}
/// Configuration for context compression.
@ -490,13 +502,13 @@ mod tests {
];
let tokens = estimate_tokens(&messages);
// JSON serialization includes: id, role, content, timestamp, etc.
// With 3 messages, the JSON overhead is significant
// Serialized JSON is typically 300-500 chars for 3 simple messages
// 500 / 4 * 1.2 = ~150 tokens
// "Hello" (5) -> ceil(5/4)+4 = 2+4 = 6
// "Hi there!" (8) -> ceil(8/4)+4 = 2+4 = 6
// "How are you?" (11) -> ceil(11/4)+4 = 3+4 = 7
// raw = 19, with 1.2x = ~23
assert!(
tokens > 50 && tokens < 300,
"Expected ~100-200 tokens (JSON overhead), got {}",
tokens > 18 && tokens < 30,
"Expected ~23 tokens, got {}",
tokens
);
}

View File

@ -1,10 +1,7 @@
use crate::agent::context_compressor::estimate_tokens;
use crate::command::context::CommandContext;
use crate::command::handler::{CommandHandler, CommandMetadata};
use crate::command::handlers::get_messages_from_session;
use crate::command::response::{CommandError, CommandResponse, MessageKind};
use crate::command::Command;
use crate::gateway::session::SessionManager;
use crate::storage::SessionStore;
use async_trait::async_trait;
use std::sync::Arc;
@ -12,20 +9,11 @@ use std::sync::Arc;
/// 获取当前话题命令处理器
pub struct GetCurrentSessionCommandHandler {
store: Arc<SessionStore>,
session_manager: Option<SessionManager>,
}
impl GetCurrentSessionCommandHandler {
pub fn new(store: Arc<SessionStore>) -> Self {
Self {
store,
session_manager: None,
}
}
pub fn with_session_manager(mut self, session_manager: SessionManager) -> Self {
self.session_manager = Some(session_manager);
self
Self { store }
}
}
@ -62,34 +50,20 @@ async fn handle_get_current_session(
let topic_id = ctx.topic_id.as_deref()
.ok_or_else(|| CommandError::new("NO_CURRENT_TOPIC", "No current topic"))?;
let chat_id = ctx.chat_id.as_deref()
.ok_or_else(|| CommandError::new("NO_CHAT_ID", "No chat id".to_string()))?;
let topic = handler
.store
.get_topic(topic_id)
.map_err(|e| CommandError::new("GET_TOPIC_ERROR", e.to_string()))?
.ok_or_else(|| CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", topic_id)))?;
// Load messages from session memory
let messages = get_messages_from_session(
&handler.session_manager,
&ctx.channel_name,
chat_id,
).await?;
let actual_message_count = messages.len();
let estimated_tokens = estimate_tokens(&messages);
let last_active = format_time_ago(topic.last_active_at);
let created_at = format_time_ago(topic.created_at);
let message = format!(
"Current Topic:\n\n Topic ID: {}\n Title: {}\n Messages: {}\n Tokens: ~{}\n Created: {}\n Last Active: {}",
"Current Topic:\n\n Topic ID: {}\n Title: {}\n Messages: {}\n Created: {}\n Last Active: {}",
topic.id,
topic.title,
actual_message_count,
estimated_tokens,
topic.message_count,
created_at,
last_active
);
@ -98,8 +72,7 @@ async fn handle_get_current_session(
.with_message(MessageKind::Notification, &message)
.with_metadata("topic_id", &topic.id)
.with_metadata("title", &topic.title)
.with_metadata("message_count", &actual_message_count.to_string())
.with_metadata("estimated_tokens", &estimated_tokens.to_string()))
.with_metadata("message_count", &topic.message_count.to_string()))
}
fn format_time_ago(timestamp_ms: i64) -> String {

View File

@ -12,35 +12,3 @@ pub use save_session::{
escape_yaml_string, format_message_content, format_timestamp,
generate_messages_markdown, generate_system_prompt_markdown,
};
use crate::bus::ChatMessage;
use crate::command::response::CommandError;
use crate::gateway::session::SessionManager;
/// 从 Session 内存获取消息历史(供命令使用)
pub async fn get_messages_from_session(
session_manager: &Option<SessionManager>,
channel_name: &str,
chat_id: &str,
) -> Result<Vec<ChatMessage>, CommandError> {
let session_manager = session_manager.as_ref().ok_or_else(|| {
CommandError::new(
"SESSION_MANAGER_NOT_SET",
"Session manager not configured".to_string(),
)
})?;
match session_manager.get(channel_name).await {
Some(session) => {
let guard = session.lock().await;
Ok(guard
.get_history(chat_id)
.map(|m| m.clone())
.unwrap_or_default())
}
None => Err(CommandError::new(
"SESSION_NOT_FOUND",
format!("Session not found for channel: {}", channel_name),
)),
}
}

View File

@ -1,14 +1,12 @@
use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider};
use crate::bus::ChatMessage;
use crate::command::context::CommandContext;
use crate::command::handler::{CommandHandler, CommandMetadata};
use crate::command::handlers::{
escape_yaml_string, format_timestamp, generate_messages_markdown,
generate_system_prompt_markdown, get_messages_from_session,
generate_system_prompt_markdown,
};
use crate::command::response::{CommandError, CommandResponse, MessageKind};
use crate::command::Command;
use crate::gateway::session::SessionManager;
use crate::storage::{SessionStore, TopicRecord};
use async_trait::async_trait;
use chrono::Local;
@ -21,7 +19,6 @@ pub async fn save_topic_to_file(
filepath: Option<String>,
store: &SessionStore,
system_prompt_provider: &dyn SystemPromptProvider,
messages: &[ChatMessage], // ← 从外部传入的消息(已压缩的 active history
) -> Result<PathBuf, String> {
// 获取话题记录
let topic = store
@ -29,6 +26,11 @@ pub async fn save_topic_to_file(
.map_err(|e| format!("Failed to get topic: {}", e))?
.ok_or_else(|| "Topic not found".to_string())?;
// 加载话题消息
let messages = store
.load_messages_for_topic(topic_id)
.map_err(|e| format!("Failed to load messages: {}", e))?;
// 获取 session 信息(用于系统提示词)
let session = store
.get_session(&topic.session_id)
@ -39,7 +41,7 @@ pub async fn save_topic_to_file(
let system_prompt = build_system_prompt(system_prompt_provider, &session, user_message_count);
// 生成 Markdown 内容
let markdown = generate_topic_markdown(&topic, &system_prompt, messages);
let markdown = generate_topic_markdown(&topic, &system_prompt, &messages);
// 确定输出路径
let output_path = resolve_topic_filepath(filepath, &topic);
@ -154,7 +156,6 @@ fn resolve_topic_filepath(filepath: Option<String>, topic: &TopicRecord) -> Path
pub struct SaveTopicCommandHandler {
store: Arc<SessionStore>,
system_prompt_provider: Arc<dyn SystemPromptProvider>,
session_manager: Option<SessionManager>,
}
impl SaveTopicCommandHandler {
@ -165,14 +166,8 @@ impl SaveTopicCommandHandler {
Self {
store,
system_prompt_provider,
session_manager: None,
}
}
pub fn with_session_manager(mut self, session_manager: SessionManager) -> Self {
self.session_manager = Some(session_manager);
self
}
}
#[async_trait]
@ -218,21 +213,7 @@ async fn handle_save_topic(
.as_deref()
.ok_or_else(|| CommandError::new("NO_TOPIC", "No active topic".to_string()))?;
let chat_id = ctx
.chat_id
.as_deref()
.ok_or_else(|| CommandError::new("NO_CHAT_ID", "No chat id".to_string()))?;
tracing::debug!(topic_id = %topic_id, chat_id = %chat_id, "Attempting to save topic");
// 从 Session 获取当前 history包含已压缩的消息
let messages = get_messages_from_session(
&handler.session_manager,
&ctx.channel_name,
chat_id,
).await?;
tracing::debug!(message_count = messages.len(), "Got messages from session");
tracing::debug!(topic_id = %topic_id, "Attempting to save topic");
// 调用保存函数
let output_path = save_topic_to_file(
@ -240,12 +221,16 @@ async fn handle_save_topic(
filepath,
&*handler.store,
&*handler.system_prompt_provider,
&messages,
)
.await
.map_err(|e| CommandError::new("SAVE_ERROR", e))?;
let message_count = messages.len();
// 获取消息数量
let message_count = handler
.store
.load_messages_for_topic(topic_id)
.map_err(|e| CommandError::new("LOAD_MESSAGES_ERROR", e.to_string()))?
.len();
Ok(CommandResponse::success(ctx.request_id)
.with_message(

View File

@ -13,6 +13,7 @@
- 当现有工具是完成任务的最直接方式时,优先使用工具。
- 除非用户明确要求改变方向,否则保持用户原本目标不变。
## 记忆处理
### 记忆检索
@ -29,6 +30,8 @@
- 纯寒暄
- 完全不依赖用户历史的直接事实问答
如果当前请求不明显属于这些例外,就默认先检索。
#### 检索方式
- 检索时应提供 queries 数组数组的数量一般需要10-12个。
@ -36,6 +39,8 @@
- 越靠近最新会话,生成关键词的比例或者权重应该更高
- 例如queries=['email', '邮件', 'email_folder_preference']
如果用户在聊持续任务、既有偏好、历史决策、项目上下文、曾经纠正过你的内容,或当前请求看起来像是延续之前的事,优先先搜记忆。
### 记忆写入
#### 写入规则
@ -53,7 +58,7 @@
- 用户说默认xxx的消息
#### 注意
- 如果你决定不再调用工具,则反思一下是否使用 memory_manage保存记忆
- 如果你决定不再调用工具,则反思一下是否使用 memory_manage
## 助理原则
@ -83,7 +88,5 @@
## 定时任务
- 默认创建静默任务silent_agent_task在独立后台会话中执行不干扰主对话
- 仅在用户明确要求时才创建普通任务agent_task在主对话中执行
- 静默模式下如需发送消息给用户prompt中需显式使用 send_session_message 工具
## 注意
- 不要通过一次调用写入一个很长的文件,请分段写入

View File

@ -56,9 +56,7 @@ impl InboundProcessor {
command_router.register(Box::new(switch_handler));
// 注册 get_current 处理器
command_router.register(Box::new(GetCurrentSessionCommandHandler::new(
store.clone(),
).with_session_manager(session_manager.clone())));
command_router.register(Box::new(GetCurrentSessionCommandHandler::new(store.clone())));
// 注册 load_session 处理器
command_router.register(Box::new(LoadSessionCommandHandler::new(store.clone())));
@ -83,7 +81,7 @@ impl InboundProcessor {
command_router.register(Box::new(SaveTopicCommandHandler::new(
store.clone(),
system_prompt_provider,
).with_session_manager(session_manager.clone())));
)));
// 注册 help 处理器(最后注册,获取所有已注册命令的元数据)
let metadata = command_router.metadata_arc();