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

View File

@ -1,10 +1,7 @@
use crate::agent::context_compressor::estimate_tokens;
use crate::command::context::CommandContext; use crate::command::context::CommandContext;
use crate::command::handler::{CommandHandler, CommandMetadata}; use crate::command::handler::{CommandHandler, CommandMetadata};
use crate::command::handlers::get_messages_from_session;
use crate::command::response::{CommandError, CommandResponse, MessageKind}; use crate::command::response::{CommandError, CommandResponse, MessageKind};
use crate::command::Command; use crate::command::Command;
use crate::gateway::session::SessionManager;
use crate::storage::SessionStore; use crate::storage::SessionStore;
use async_trait::async_trait; use async_trait::async_trait;
use std::sync::Arc; use std::sync::Arc;
@ -12,20 +9,11 @@ use std::sync::Arc;
/// 获取当前话题命令处理器 /// 获取当前话题命令处理器
pub struct GetCurrentSessionCommandHandler { pub struct GetCurrentSessionCommandHandler {
store: Arc<SessionStore>, store: Arc<SessionStore>,
session_manager: Option<SessionManager>,
} }
impl GetCurrentSessionCommandHandler { impl GetCurrentSessionCommandHandler {
pub fn new(store: Arc<SessionStore>) -> Self { pub fn new(store: Arc<SessionStore>) -> Self {
Self { Self { store }
store,
session_manager: None,
}
}
pub fn with_session_manager(mut self, session_manager: SessionManager) -> Self {
self.session_manager = Some(session_manager);
self
} }
} }
@ -62,34 +50,20 @@ async fn handle_get_current_session(
let topic_id = ctx.topic_id.as_deref() let topic_id = ctx.topic_id.as_deref()
.ok_or_else(|| CommandError::new("NO_CURRENT_TOPIC", "No current topic"))?; .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 let topic = handler
.store .store
.get_topic(topic_id) .get_topic(topic_id)
.map_err(|e| CommandError::new("GET_TOPIC_ERROR", e.to_string()))? .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)))?; .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 last_active = format_time_ago(topic.last_active_at);
let created_at = format_time_ago(topic.created_at); let created_at = format_time_ago(topic.created_at);
let message = format!( 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.id,
topic.title, topic.title,
actual_message_count, topic.message_count,
estimated_tokens,
created_at, created_at,
last_active last_active
); );
@ -98,8 +72,7 @@ async fn handle_get_current_session(
.with_message(MessageKind::Notification, &message) .with_message(MessageKind::Notification, &message)
.with_metadata("topic_id", &topic.id) .with_metadata("topic_id", &topic.id)
.with_metadata("title", &topic.title) .with_metadata("title", &topic.title)
.with_metadata("message_count", &actual_message_count.to_string()) .with_metadata("message_count", &topic.message_count.to_string()))
.with_metadata("estimated_tokens", &estimated_tokens.to_string()))
} }
fn format_time_ago(timestamp_ms: i64) -> 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, escape_yaml_string, format_message_content, format_timestamp,
generate_messages_markdown, generate_system_prompt_markdown, 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::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider};
use crate::bus::ChatMessage;
use crate::command::context::CommandContext; use crate::command::context::CommandContext;
use crate::command::handler::{CommandHandler, CommandMetadata}; use crate::command::handler::{CommandHandler, CommandMetadata};
use crate::command::handlers::{ use crate::command::handlers::{
escape_yaml_string, format_timestamp, generate_messages_markdown, 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::response::{CommandError, CommandResponse, MessageKind};
use crate::command::Command; use crate::command::Command;
use crate::gateway::session::SessionManager;
use crate::storage::{SessionStore, TopicRecord}; use crate::storage::{SessionStore, TopicRecord};
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Local; use chrono::Local;
@ -21,7 +19,6 @@ pub async fn save_topic_to_file(
filepath: Option<String>, filepath: Option<String>,
store: &SessionStore, store: &SessionStore,
system_prompt_provider: &dyn SystemPromptProvider, system_prompt_provider: &dyn SystemPromptProvider,
messages: &[ChatMessage], // ← 从外部传入的消息(已压缩的 active history
) -> Result<PathBuf, String> { ) -> Result<PathBuf, String> {
// 获取话题记录 // 获取话题记录
let topic = store let topic = store
@ -29,6 +26,11 @@ pub async fn save_topic_to_file(
.map_err(|e| format!("Failed to get topic: {}", e))? .map_err(|e| format!("Failed to get topic: {}", e))?
.ok_or_else(|| "Topic not found".to_string())?; .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 信息(用于系统提示词) // 获取 session 信息(用于系统提示词)
let session = store let session = store
.get_session(&topic.session_id) .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); let system_prompt = build_system_prompt(system_prompt_provider, &session, user_message_count);
// 生成 Markdown 内容 // 生成 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); 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 { pub struct SaveTopicCommandHandler {
store: Arc<SessionStore>, store: Arc<SessionStore>,
system_prompt_provider: Arc<dyn SystemPromptProvider>, system_prompt_provider: Arc<dyn SystemPromptProvider>,
session_manager: Option<SessionManager>,
} }
impl SaveTopicCommandHandler { impl SaveTopicCommandHandler {
@ -165,14 +166,8 @@ impl SaveTopicCommandHandler {
Self { Self {
store, store,
system_prompt_provider, 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] #[async_trait]
@ -218,21 +213,7 @@ async fn handle_save_topic(
.as_deref() .as_deref()
.ok_or_else(|| CommandError::new("NO_TOPIC", "No active topic".to_string()))?; .ok_or_else(|| CommandError::new("NO_TOPIC", "No active topic".to_string()))?;
let chat_id = ctx tracing::debug!(topic_id = %topic_id, "Attempting to save topic");
.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");
// 调用保存函数 // 调用保存函数
let output_path = save_topic_to_file( let output_path = save_topic_to_file(
@ -240,12 +221,16 @@ async fn handle_save_topic(
filepath, filepath,
&*handler.store, &*handler.store,
&*handler.system_prompt_provider, &*handler.system_prompt_provider,
&messages,
) )
.await .await
.map_err(|e| CommandError::new("SAVE_ERROR", e))?; .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) Ok(CommandResponse::success(ctx.request_id)
.with_message( .with_message(

View File

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

View File

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